ruby-merge 7.0.0 → 7.1.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
- checksums.yaml.gz.sig +0 -0
- data/LICENSE.md +13 -0
- data/README.md +289 -0
- data/lib/ruby/merge/provider.rb +77 -0
- data/lib/ruby/merge/version.rb +5 -3
- data/lib/ruby/merge.rb +2715 -313
- data/lib/ruby-merge.rb +7 -1
- data/sig/ruby/merge.rbs +21 -0
- data.tar.gz.sig +0 -0
- metadata +265 -14
- metadata.gz.sig +0 -0
data/lib/ruby/merge.rb
CHANGED
|
@@ -1,42 +1,75 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
3
|
+
require 'digest'
|
|
4
|
+
require 'prism/merge'
|
|
5
|
+
require 'tree_haver'
|
|
6
|
+
require 'ast/merge'
|
|
7
|
+
require_relative 'merge/version'
|
|
5
8
|
|
|
6
9
|
module Ruby
|
|
7
10
|
module Merge
|
|
8
11
|
extend self
|
|
12
|
+
include Ast::Merge::SourceRegionReportSupport
|
|
9
13
|
|
|
10
|
-
PACKAGE_NAME =
|
|
14
|
+
PACKAGE_NAME = 'ruby-merge'
|
|
11
15
|
TREE_SITTER_BACKEND = TreeHaver::KREUZBERG_LANGUAGE_PACK_BACKEND
|
|
12
|
-
DESTINATION_WINS_ARRAY_POLICY = { surface:
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
16
|
+
DESTINATION_WINS_ARRAY_POLICY = { surface: 'array', name: 'destination_wins_array' }.freeze
|
|
17
|
+
DEFAULT_METHOD_MOVE_POLICY = 'destination_order'
|
|
18
|
+
BACKEND_REGISTRY = Struct.new(:registered, :mutex).new(false, Mutex.new)
|
|
19
|
+
TslpSpan = Struct.new(:start_row, :start_col, :end_row, :end_col, keyword_init: true)
|
|
20
|
+
TslpStructureItem = Struct.new(:kind, :name, :span, keyword_init: true)
|
|
21
|
+
TslpImportItem = Struct.new(:source, :span, keyword_init: true)
|
|
22
|
+
TslpProcessAnalysis = Struct.new(:structure, :imports, keyword_init: true)
|
|
23
|
+
PERCENT_ARRAY_DELIMITER_PAIRS = {
|
|
24
|
+
'[' => ']',
|
|
25
|
+
'(' => ')',
|
|
26
|
+
'{' => '}',
|
|
27
|
+
'<' => '>'
|
|
28
|
+
}.freeze
|
|
29
|
+
REQUIRE_PATTERN = /^\s*require(?:_relative)?\s+["']([^"']+)["']/
|
|
30
|
+
CLASS_PATTERN = /^\s*class\s+([A-Z]\w*(?:::\w+)*)/
|
|
31
|
+
MODULE_PATTERN = /^\s*module\s+([A-Z]\w*(?:::\w+)*)/
|
|
32
|
+
DEF_PATTERN = %r{^\s*def\s+((?:self\.)?)([a-zA-Z_]\w*[!?=]?|\[\]=?|\+@|-@|\*\*|<<|>>|<=>|===|==|=~|!~|!=|[+\-*/%&|^<>]=?|[!~`])}
|
|
33
|
+
CONSTANT_ASSIGNMENT_PATTERN = /^(\s*)([A-Z]\w*)\s*=/
|
|
34
|
+
CONSTANT_HASH_ASSIGNMENT_PATTERN = /^(\s*)([A-Z]\w*)\s*=\s*\{/
|
|
35
|
+
|
|
36
|
+
def register_backend!
|
|
37
|
+
BACKEND_REGISTRY.mutex.synchronize do
|
|
38
|
+
return if BACKEND_REGISTRY.registered
|
|
39
|
+
|
|
40
|
+
TreeHaver::BackendRegistry.register(TREE_SITTER_BACKEND)
|
|
41
|
+
|
|
42
|
+
grammar_finder = TreeHaver::GrammarFinder.new(:ruby)
|
|
43
|
+
grammar_finder.register! if grammar_finder.available?
|
|
44
|
+
|
|
45
|
+
BACKEND_REGISTRY.registered = true
|
|
46
|
+
end
|
|
47
|
+
end
|
|
24
48
|
|
|
25
49
|
def ruby_feature_profile
|
|
26
50
|
{
|
|
27
|
-
family:
|
|
28
|
-
supported_dialects: [
|
|
51
|
+
family: 'ruby',
|
|
52
|
+
supported_dialects: ['ruby'],
|
|
29
53
|
supported_policies: [DESTINATION_WINS_ARRAY_POLICY]
|
|
30
54
|
}
|
|
31
55
|
end
|
|
32
56
|
|
|
33
57
|
def available_ruby_backends
|
|
34
|
-
[TREE_SITTER_BACKEND]
|
|
58
|
+
ruby_backend_available_for_analysis?(TREE_SITTER_BACKEND.id) ? [TREE_SITTER_BACKEND] : []
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def ruby_tslp_capability_profile
|
|
62
|
+
{
|
|
63
|
+
import_records: TreeHaver::BackendRegistry.tag_available?(:tslp_ruby_import_records),
|
|
64
|
+
top_level_call_records: TreeHaver::BackendRegistry.tag_available?(:tslp_ruby_top_level_call_records)
|
|
65
|
+
}
|
|
35
66
|
end
|
|
36
67
|
|
|
37
68
|
def ruby_backend_feature_profile(backend: nil)
|
|
38
|
-
requested = backend
|
|
39
|
-
|
|
69
|
+
requested = requested_tree_sitter_backend_id(backend)
|
|
70
|
+
unless ruby_backend_available_for_analysis?(requested)
|
|
71
|
+
return unsupported_feature_result("Unsupported Ruby backend #{requested}.")
|
|
72
|
+
end
|
|
40
73
|
|
|
41
74
|
ruby_feature_profile.merge(
|
|
42
75
|
backend: requested,
|
|
@@ -59,77 +92,214 @@ module Ruby
|
|
|
59
92
|
}
|
|
60
93
|
end
|
|
61
94
|
|
|
95
|
+
def ruby_backend_available_for_analysis?(backend_id)
|
|
96
|
+
register_backend!
|
|
97
|
+
|
|
98
|
+
if backend_id.to_s.empty?
|
|
99
|
+
TreeHaver.parser_for(:ruby, backend_type: :tree_sitter)
|
|
100
|
+
else
|
|
101
|
+
TreeHaver.with_backend(backend_id) { TreeHaver.parser_for(:ruby, backend_type: :tree_sitter) }
|
|
102
|
+
end
|
|
103
|
+
true
|
|
104
|
+
rescue TreeHaver::Error, ArgumentError
|
|
105
|
+
false
|
|
106
|
+
end
|
|
107
|
+
|
|
62
108
|
def parse_ruby(source, dialect, backend: nil)
|
|
63
|
-
requested = backend.to_s.empty? ?
|
|
64
|
-
return unsupported_feature_result("Unsupported Ruby dialect #{dialect}.") unless dialect ==
|
|
65
|
-
return unsupported_feature_result("Unsupported Ruby backend #{requested}.") unless requested == TREE_SITTER_BACKEND.id
|
|
109
|
+
requested = backend.to_s.empty? ? nil : backend.to_s
|
|
110
|
+
return unsupported_feature_result("Unsupported Ruby dialect #{dialect}.") unless dialect == 'ruby'
|
|
66
111
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
112
|
+
unless ruby_backend_available_for_analysis?(requested)
|
|
113
|
+
diagnostic_backend = requested || TreeHaver.current_backend_id || 'tree-sitter'
|
|
114
|
+
return unsupported_feature_result("Unsupported Ruby backend #{diagnostic_backend}.")
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
tree = parse_tree_sitter_source(:ruby, source, backend: requested)
|
|
118
|
+
collect_parse_errors(tree.root_node)
|
|
71
119
|
|
|
120
|
+
process_analysis = ruby_process_analysis_from_tree(source, tree.root_node)
|
|
72
121
|
{
|
|
73
122
|
ok: true,
|
|
74
123
|
diagnostics: [],
|
|
75
|
-
analysis: analyze_ruby_document(source),
|
|
124
|
+
analysis: analyze_ruby_document(source, process_analysis: process_analysis),
|
|
76
125
|
policies: []
|
|
77
126
|
}
|
|
127
|
+
rescue TreeHaver::Error, StandardError => e
|
|
128
|
+
parse_failure_result(e)
|
|
78
129
|
end
|
|
79
130
|
|
|
131
|
+
def parse_tree_sitter_source(language, source, backend: nil)
|
|
132
|
+
if backend
|
|
133
|
+
TreeHaver.with_backend(backend) { TreeHaver.parser_for(language, backend_type: :tree_sitter).parse(source) }
|
|
134
|
+
else
|
|
135
|
+
TreeHaver.parser_for(language, backend_type: :tree_sitter).parse(source)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
private_class_method :parse_tree_sitter_source
|
|
139
|
+
|
|
140
|
+
def requested_tree_sitter_backend_id(backend)
|
|
141
|
+
return backend.to_s unless backend.to_s.empty?
|
|
142
|
+
|
|
143
|
+
contextual = TreeHaver.current_backend_id || ENV['TREE_HAVER_BACKEND']
|
|
144
|
+
contextual.to_s.empty? || contextual.to_s == 'auto' ? TREE_SITTER_BACKEND.id : contextual.to_s
|
|
145
|
+
end
|
|
146
|
+
private_class_method :requested_tree_sitter_backend_id
|
|
147
|
+
|
|
80
148
|
def match_ruby_owners(template, destination)
|
|
81
|
-
|
|
82
|
-
template_paths = template[:owners].to_h { |owner| [owner[:path], true] }
|
|
83
|
-
{
|
|
84
|
-
matched: template[:owners]
|
|
85
|
-
.filter { |owner| destination_paths[owner[:path]] }
|
|
86
|
-
.map { |owner| { template_path: owner[:path], destination_path: owner[:path] } },
|
|
87
|
-
unmatched_template: template[:owners].map { |owner| owner[:path] }.reject { |path| destination_paths[path] },
|
|
88
|
-
unmatched_destination: destination[:owners].map { |owner| owner[:path] }.reject { |path| template_paths[path] }
|
|
89
|
-
}
|
|
149
|
+
Ast::Merge::OwnerSelection.match_by_path(template, destination)
|
|
90
150
|
end
|
|
91
151
|
|
|
92
|
-
def
|
|
152
|
+
def ruby_method_move_detection(template_source, destination_source, dialect)
|
|
153
|
+
return unsupported_feature_result("Unsupported Ruby dialect #{dialect}.") unless dialect == 'ruby'
|
|
154
|
+
|
|
155
|
+
template_methods = ruby_method_projection(template_source, revision: 'template')
|
|
156
|
+
destination_methods = ruby_method_projection(destination_source, revision: 'destination')
|
|
157
|
+
destination_by_signature = destination_methods.to_h { |entry| [entry[:signature], entry] }
|
|
158
|
+
template_signatures = template_methods.map { |entry| entry[:signature] }.to_h { |signature| [signature, true] }
|
|
159
|
+
|
|
160
|
+
matches = template_methods.filter_map do |template_entry|
|
|
161
|
+
destination_entry = destination_by_signature[template_entry[:signature]]
|
|
162
|
+
next unless destination_entry
|
|
163
|
+
|
|
164
|
+
moved = template_entry[:index] != destination_entry[:index] || template_entry[:parent_path] != destination_entry[:parent_path]
|
|
165
|
+
Ast::Merge::MoveDetectionMatch.new(
|
|
166
|
+
from_path: template_entry[:path],
|
|
167
|
+
to_path: destination_entry[:path],
|
|
168
|
+
from_node_id: template_entry[:node_id],
|
|
169
|
+
to_node_id: destination_entry[:node_id],
|
|
170
|
+
signature: template_entry[:signature],
|
|
171
|
+
moved: moved,
|
|
172
|
+
from_parent_path: template_entry[:parent_path],
|
|
173
|
+
to_parent_path: destination_entry[:parent_path],
|
|
174
|
+
from_index: template_entry[:index],
|
|
175
|
+
to_index: destination_entry[:index],
|
|
176
|
+
confidence: moved ? 0.98 : 0.9,
|
|
177
|
+
diagnostics: [moved ? 'same Ruby method signature observed at a different sibling position' : 'same Ruby method signature observed at the same sibling position']
|
|
178
|
+
)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
matched_template_signatures = matches.map(&:signature).to_h { |signature| [signature, true] }
|
|
182
|
+
Ast::Merge::MoveDetectionMatchingReport.new(
|
|
183
|
+
matching_id: 'ruby-method-move-detection',
|
|
184
|
+
strategy: 'move_detection',
|
|
185
|
+
from_revision: 'template',
|
|
186
|
+
to_revision: 'destination',
|
|
187
|
+
capability: Ast::Merge::MoveDetectionCapability.new(
|
|
188
|
+
name: 'move_detection',
|
|
189
|
+
enabled: true,
|
|
190
|
+
default_enabled: false,
|
|
191
|
+
requires_stable_node_identity: true
|
|
192
|
+
),
|
|
193
|
+
matches: matches,
|
|
194
|
+
unmatched_from: template_methods.reject do |entry|
|
|
195
|
+
matched_template_signatures[entry[:signature]]
|
|
196
|
+
end.map { |entry| entry[:path] },
|
|
197
|
+
unmatched_to: destination_methods.reject do |entry|
|
|
198
|
+
template_signatures[entry[:signature]]
|
|
199
|
+
end.map { |entry| entry[:path] },
|
|
200
|
+
diagnostics: ['Ruby method move detection uses generic move-detection matching over receiver-aware method projections']
|
|
201
|
+
).to_h
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def merge_ruby(template_source, destination_source, dialect, merge_template_requires: false,
|
|
205
|
+
method_move_policy: DEFAULT_METHOD_MOVE_POLICY)
|
|
93
206
|
template = parse_ruby(template_source, dialect)
|
|
94
207
|
return template unless template[:ok]
|
|
95
208
|
|
|
209
|
+
method_move_policy = normalize_method_move_policy(method_move_policy)
|
|
210
|
+
|
|
96
211
|
destination = parse_ruby(destination_source, dialect)
|
|
97
212
|
unless destination[:ok]
|
|
98
213
|
return {
|
|
99
214
|
ok: false,
|
|
100
215
|
diagnostics: destination[:diagnostics].map do |diagnostic|
|
|
101
|
-
diagnostic[:category] ==
|
|
216
|
+
diagnostic[:category] == 'parse_error' ? diagnostic.merge(category: 'destination_parse_error') : diagnostic
|
|
102
217
|
end,
|
|
103
218
|
policies: []
|
|
104
219
|
}
|
|
105
220
|
end
|
|
106
221
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
222
|
+
destination_context = ruby_tslp_merge_context(destination.fetch(:analysis), role: 'destination')
|
|
223
|
+
return destination_context unless destination_context[:ok]
|
|
224
|
+
|
|
225
|
+
template_context = ruby_tslp_merge_context(template.fetch(:analysis), role: 'template')
|
|
226
|
+
return template_context unless template_context[:ok]
|
|
227
|
+
|
|
228
|
+
destination_requires = destination_context.fetch(:requires)
|
|
229
|
+
template_requires = template_context.fetch(:requires)
|
|
230
|
+
destination_declarations = destination_context.fetch(:declarations)
|
|
231
|
+
template_declarations = template_context.fetch(:declarations)
|
|
232
|
+
template_declarations_by_key = template_declarations.to_h { |entry| [entry[:merge_key], entry] }
|
|
233
|
+
intra_owner_merges = ruby_intra_owner_merge_plan(template_declarations, destination_declarations)
|
|
234
|
+
namespace_conflicts = ruby_namespace_form_conflicts(template_declarations, destination_declarations)
|
|
235
|
+
unless namespace_conflicts.empty? || TreeHaver::BackendRegistry.tag_available?(:tslp_ruby_namespace_form_equivalence)
|
|
236
|
+
conflicts = namespace_conflicts.join(', ')
|
|
237
|
+
return unsupported_feature_result(
|
|
238
|
+
'ruby-merge cannot reconcile equivalent Ruby namespace declaration forms with the active TSLP records: ' \
|
|
239
|
+
"#{conflicts}. Use prism-merge for native Ruby merging, or report missing Ruby namespace ownership " \
|
|
240
|
+
'records to tree-sitter-language-pack.'
|
|
241
|
+
)
|
|
242
|
+
end
|
|
243
|
+
if !namespace_conflicts.empty? && TreeHaver::BackendRegistry.tag_available?(:tslp_ruby_namespace_form_equivalence)
|
|
244
|
+
template_declarations += qualified_nested_declaration_entries(template_declarations)
|
|
245
|
+
template_declarations_by_key = template_declarations.to_h { |entry| [entry[:merge_key], entry] }
|
|
246
|
+
end
|
|
247
|
+
destination_paths = destination_declarations.to_h { |entry| [entry[:merge_key], true] }
|
|
114
248
|
sections = []
|
|
115
|
-
preamble =
|
|
116
|
-
sections << preamble unless preamble.empty?
|
|
117
|
-
requires = merge_template_requires
|
|
249
|
+
preamble = destination_context.fetch(:preamble)
|
|
250
|
+
sections << { text: preamble } unless preamble.empty?
|
|
251
|
+
requires = if merge_template_requires
|
|
252
|
+
merge_ruby_requires(destination_requires,
|
|
253
|
+
template_requires)
|
|
254
|
+
else
|
|
255
|
+
destination_requires
|
|
256
|
+
end
|
|
118
257
|
require_block = requires.map { |entry| entry[:text] }.join("\n").strip
|
|
119
|
-
sections << require_block unless require_block.empty?
|
|
120
|
-
sections.concat(
|
|
121
|
-
|
|
258
|
+
sections << ruby_top_level_section(require_block, requires) unless require_block.empty?
|
|
259
|
+
sections.concat(
|
|
260
|
+
destination_declarations.map do |entry|
|
|
261
|
+
ruby_top_level_section(
|
|
262
|
+
merge_ruby_declaration_entry(template_declarations_by_key[entry[:merge_key]], entry)[:text],
|
|
263
|
+
[entry]
|
|
264
|
+
)
|
|
265
|
+
end
|
|
266
|
+
)
|
|
122
267
|
sections.concat(
|
|
123
|
-
template_declarations.reject
|
|
268
|
+
template_declarations.reject do |entry|
|
|
269
|
+
destination_paths[entry[:merge_key]] ||
|
|
270
|
+
namespace_wrapper_matched?(entry, template_declarations, destination_paths)
|
|
271
|
+
end.map { |entry| { text: entry[:text] } }
|
|
124
272
|
)
|
|
273
|
+
destination_footer = destination_context.fetch(:footer)
|
|
274
|
+
sections << { text: destination_footer } unless destination_footer.empty?
|
|
125
275
|
|
|
126
|
-
output =
|
|
276
|
+
output = emit_ruby_top_level_sections(destination_source, sections)
|
|
277
|
+
matching_reports = [ruby_method_move_detection(template_source, destination_source, dialect)]
|
|
278
|
+
moved_method_count = matching_reports.sum do |report|
|
|
279
|
+
Array(report[:matches]).count { |entry| entry[:moved] }
|
|
280
|
+
end
|
|
127
281
|
|
|
128
282
|
{
|
|
129
283
|
ok: true,
|
|
130
284
|
diagnostics: [],
|
|
131
|
-
output:
|
|
132
|
-
policies: [DESTINATION_WINS_ARRAY_POLICY]
|
|
285
|
+
output: output,
|
|
286
|
+
policies: [DESTINATION_WINS_ARRAY_POLICY],
|
|
287
|
+
matching_reports: matching_reports,
|
|
288
|
+
merge_planning: {
|
|
289
|
+
method_move_policy: method_move_policy,
|
|
290
|
+
method_move_detection: {
|
|
291
|
+
matching_id: 'ruby-tslp-method-move-detection',
|
|
292
|
+
moved_method_count: moved_method_count,
|
|
293
|
+
preserves_destination_order: method_move_policy == DEFAULT_METHOD_MOVE_POLICY,
|
|
294
|
+
suppresses_duplicate_moved_methods: method_move_policy == DEFAULT_METHOD_MOVE_POLICY,
|
|
295
|
+
override_scope: 'per_file_recipe'
|
|
296
|
+
},
|
|
297
|
+
intra_owner_merges: {
|
|
298
|
+
strategy: 'destination_wins_scoped_owner_body',
|
|
299
|
+
merge_count: intra_owner_merges.length,
|
|
300
|
+
merges: intra_owner_merges
|
|
301
|
+
}
|
|
302
|
+
}
|
|
133
303
|
}
|
|
134
304
|
end
|
|
135
305
|
|
|
@@ -137,34 +307,34 @@ module Ruby
|
|
|
137
307
|
analysis[:discovered_surfaces] || []
|
|
138
308
|
end
|
|
139
309
|
|
|
140
|
-
def ruby_delegated_child_operations(analysis, parent_operation_id:
|
|
310
|
+
def ruby_delegated_child_operations(analysis, parent_operation_id: 'ruby-document-0')
|
|
141
311
|
surfaces = ruby_discovered_surfaces(analysis)
|
|
142
312
|
doc_operation_ids = {}
|
|
143
313
|
operations = []
|
|
144
314
|
|
|
145
315
|
surfaces.each_with_index do |surface, index|
|
|
146
|
-
next unless surface[:surface_kind] ==
|
|
316
|
+
next unless surface[:surface_kind] == 'ruby_doc_comment'
|
|
147
317
|
|
|
148
318
|
operation_id = "ruby-doc-comment-#{index}"
|
|
149
319
|
doc_operation_ids[surface[:address]] = operation_id
|
|
150
320
|
operations << Ast::Merge.delegated_child_operation(
|
|
151
321
|
operation_id: operation_id,
|
|
152
322
|
parent_operation_id: parent_operation_id,
|
|
153
|
-
requested_strategy:
|
|
154
|
-
language_chain: [
|
|
323
|
+
requested_strategy: 'delegate_child_surface',
|
|
324
|
+
language_chain: ['ruby', surface[:effective_language]],
|
|
155
325
|
surface: surface
|
|
156
326
|
)
|
|
157
327
|
end
|
|
158
328
|
|
|
159
329
|
example_index = 0
|
|
160
330
|
surfaces.each do |surface|
|
|
161
|
-
next unless surface[:surface_kind] ==
|
|
331
|
+
next unless surface[:surface_kind] == 'yard_example_block'
|
|
162
332
|
|
|
163
333
|
operations << Ast::Merge.delegated_child_operation(
|
|
164
334
|
operation_id: "yard-example-#{example_index}",
|
|
165
335
|
parent_operation_id: doc_operation_ids.fetch(surface[:parent_address], parent_operation_id),
|
|
166
|
-
requested_strategy:
|
|
167
|
-
language_chain: [
|
|
336
|
+
requested_strategy: 'delegate_child_surface',
|
|
337
|
+
language_chain: ['ruby', 'yard', surface[:effective_language]],
|
|
168
338
|
surface: surface
|
|
169
339
|
)
|
|
170
340
|
example_index += 1
|
|
@@ -173,6 +343,721 @@ module Ruby
|
|
|
173
343
|
operations
|
|
174
344
|
end
|
|
175
345
|
|
|
346
|
+
def ruby_source_regions(source)
|
|
347
|
+
lines = normalize_source(source).lines(chomp: true)
|
|
348
|
+
owners = top_level_source_region_owners(lines)
|
|
349
|
+
|
|
350
|
+
{
|
|
351
|
+
regions: source_interleaved_regions_for_report(lines: lines, owners: owners),
|
|
352
|
+
trailing_newline: normalize_source(source).end_with?("\n")
|
|
353
|
+
}
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def ruby_source_owner_identity_profile(source)
|
|
357
|
+
identities = collect_ruby_declaration_entries(source).flat_map do |entry|
|
|
358
|
+
declaration_identity = source_owner_identity_entry(
|
|
359
|
+
kind: entry[:kind],
|
|
360
|
+
name: entry[:name],
|
|
361
|
+
parent_scope: '/',
|
|
362
|
+
address: entry[:path],
|
|
363
|
+
content: entry[:text]
|
|
364
|
+
)
|
|
365
|
+
method_identities = direct_body_method_entries(entry[:text]).map do |method_entry|
|
|
366
|
+
source_owner_identity_entry(
|
|
367
|
+
kind: 'method',
|
|
368
|
+
name: method_entry[:signature],
|
|
369
|
+
parent_scope: entry[:path],
|
|
370
|
+
address: "#{entry[:path]}/methods/#{method_entry[:signature]}",
|
|
371
|
+
content: method_entry[:body_text]
|
|
372
|
+
)
|
|
373
|
+
end
|
|
374
|
+
[declaration_identity, *method_identities]
|
|
375
|
+
end
|
|
376
|
+
add_source_owner_occurrence_indexes(identities)
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def ruby_source_owner_identity_matches(template_source, destination_source)
|
|
380
|
+
template_identities = ruby_source_owner_identity_profile(template_source)
|
|
381
|
+
destination_identities = ruby_source_owner_identity_profile(destination_source)
|
|
382
|
+
destination_groups = destination_identities.group_by { |identity| identity[:structural_identity] }
|
|
383
|
+
template_identities.group_by { |identity| identity[:structural_identity] }
|
|
384
|
+
matched_destination_addresses = {}
|
|
385
|
+
|
|
386
|
+
matches = template_identities.filter_map do |template_identity|
|
|
387
|
+
destination_identity = destination_groups.fetch(template_identity[:structural_identity], []).find do |candidate|
|
|
388
|
+
candidate[:occurrence_index] == template_identity[:occurrence_index]
|
|
389
|
+
end
|
|
390
|
+
next unless destination_identity
|
|
391
|
+
|
|
392
|
+
matched_destination_addresses[destination_identity[:address]] = true
|
|
393
|
+
{
|
|
394
|
+
template_address: template_identity[:address],
|
|
395
|
+
destination_address: destination_identity[:address],
|
|
396
|
+
structural_identity: template_identity[:structural_identity],
|
|
397
|
+
occurrence_index: template_identity[:occurrence_index],
|
|
398
|
+
confidence: 'structural_ordered'
|
|
399
|
+
}
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
matched_template_addresses = matches.to_h { |match| [match[:template_address], true] }
|
|
403
|
+
{
|
|
404
|
+
confidence_profile: ruby_source_owner_match_confidence_profile,
|
|
405
|
+
matches: matches,
|
|
406
|
+
unmatched_template: template_identities.reject do |identity|
|
|
407
|
+
matched_template_addresses[identity[:address]]
|
|
408
|
+
end.map { |identity| identity[:address] },
|
|
409
|
+
unmatched_destination: destination_identities.reject do |identity|
|
|
410
|
+
matched_destination_addresses[identity[:address]]
|
|
411
|
+
end.map { |identity| identity[:address] },
|
|
412
|
+
diagnostics: [
|
|
413
|
+
{
|
|
414
|
+
severity: 'info',
|
|
415
|
+
category: 'source_owner_identity_matching',
|
|
416
|
+
message: 'Ruby source-owner matching reports confidence per match and uses ordered structural pairing for duplicate identities.'
|
|
417
|
+
}
|
|
418
|
+
]
|
|
419
|
+
}
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def ruby_ambiguous_source_owner_identity_report(source)
|
|
423
|
+
identities = ruby_source_owner_identity_profile(source)
|
|
424
|
+
ambiguities = identities
|
|
425
|
+
.group_by { |identity| identity[:structural_identity] }
|
|
426
|
+
.filter_map do |structural_identity, entries|
|
|
427
|
+
next if entries.length < 2
|
|
428
|
+
|
|
429
|
+
{
|
|
430
|
+
structural_identity: structural_identity,
|
|
431
|
+
occurrence_count: entries.length,
|
|
432
|
+
addresses: entries.map { |entry| entry[:address] },
|
|
433
|
+
ambiguity_kind: 'duplicate_structural_identity',
|
|
434
|
+
resolution_model: 'ordered_cursor',
|
|
435
|
+
confidence: 'structural_ordered'
|
|
436
|
+
}
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
{
|
|
440
|
+
ambiguities: ambiguities,
|
|
441
|
+
diagnostics: if ambiguities.empty?
|
|
442
|
+
[]
|
|
443
|
+
else
|
|
444
|
+
[
|
|
445
|
+
{
|
|
446
|
+
severity: 'warning',
|
|
447
|
+
category: 'ambiguous_source_owner_identity',
|
|
448
|
+
message: 'Repeated Ruby source-owner identities require ordered cursor matching.'
|
|
449
|
+
}
|
|
450
|
+
]
|
|
451
|
+
end
|
|
452
|
+
}
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def ruby_source_owner_match_confidence_profile
|
|
456
|
+
{
|
|
457
|
+
levels: [
|
|
458
|
+
{
|
|
459
|
+
name: 'exact',
|
|
460
|
+
meaning: 'same structural identity, occurrence index, and content identity'
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
name: 'structural_ordered',
|
|
464
|
+
meaning: 'same structural identity and occurrence index'
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
name: 'content_hash',
|
|
468
|
+
meaning: 'same content-derived identity when structural identity is ambiguous'
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
name: 'token_similar',
|
|
472
|
+
meaning: 'similar token content below exact content identity'
|
|
473
|
+
},
|
|
474
|
+
{
|
|
475
|
+
name: 'unresolved',
|
|
476
|
+
meaning: 'identity is ambiguous and must not be auto-matched'
|
|
477
|
+
}
|
|
478
|
+
]
|
|
479
|
+
}
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
def ruby_fallback_policy_profile
|
|
483
|
+
{
|
|
484
|
+
policy_id: 'ruby-source-fallback-policy',
|
|
485
|
+
baseline_provider: {
|
|
486
|
+
provider_id: 'host_baseline_merge',
|
|
487
|
+
integration_point: true
|
|
488
|
+
},
|
|
489
|
+
scopes: %w[node subtree owned_region whole_file],
|
|
490
|
+
triggers: [
|
|
491
|
+
{ reason: 'binary_input', scope: 'whole_file' },
|
|
492
|
+
{ reason: 'unsupported_structural_merge_capability', scope: 'whole_file' },
|
|
493
|
+
{ reason: 'no_structural_owners', scope: 'whole_file' },
|
|
494
|
+
{ reason: 'both_branches_create_file', scope: 'whole_file' },
|
|
495
|
+
{ reason: 'excessive_duplicate_identities', scope: 'owned_region' },
|
|
496
|
+
{ reason: 'timeout_or_resource_budget', scope: 'whole_file' },
|
|
497
|
+
{ reason: 'backend_diagnostic_threshold', scope: 'owned_region' }
|
|
498
|
+
],
|
|
499
|
+
reporting_fields: %w[activated reason scope selected_baseline structured_result_discarded]
|
|
500
|
+
}
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
def ruby_fallback_activation_report(reason:, scope:, selected_baseline: 'host_baseline_merge',
|
|
504
|
+
structured_result_discarded: true)
|
|
505
|
+
{
|
|
506
|
+
activated: true,
|
|
507
|
+
reason: reason,
|
|
508
|
+
scope: scope,
|
|
509
|
+
selected_baseline: selected_baseline,
|
|
510
|
+
structured_result_discarded: structured_result_discarded,
|
|
511
|
+
policy_id: ruby_fallback_policy_profile.fetch(:policy_id),
|
|
512
|
+
diagnostics: [
|
|
513
|
+
{
|
|
514
|
+
severity: 'warning',
|
|
515
|
+
category: 'fallback_applied',
|
|
516
|
+
message: "Ruby source fallback activated for #{reason} at #{scope} scope."
|
|
517
|
+
}
|
|
518
|
+
]
|
|
519
|
+
}
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
def ruby_never_worse_fallback_mode
|
|
523
|
+
{
|
|
524
|
+
mode_id: 'never_worse_than_baseline',
|
|
525
|
+
enabled: true,
|
|
526
|
+
baseline_provider: ruby_fallback_policy_profile.dig(:baseline_provider, :provider_id),
|
|
527
|
+
comparison: {
|
|
528
|
+
conflict_count: 'structured_must_not_exceed_baseline',
|
|
529
|
+
conflict_scope: 'structured_must_not_be_broader_than_baseline',
|
|
530
|
+
data_loss: 'structured_must_not_drop_clean_branch_content'
|
|
531
|
+
},
|
|
532
|
+
fallback_action: 'discard_structured_result_and_use_baseline',
|
|
533
|
+
diagnostics: [
|
|
534
|
+
{
|
|
535
|
+
severity: 'info',
|
|
536
|
+
category: 'never_worse_fallback_mode',
|
|
537
|
+
message: 'Ruby fallback comparison mode treats the host baseline merge as the safety floor.'
|
|
538
|
+
}
|
|
539
|
+
]
|
|
540
|
+
}
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
def ruby_post_merge_validation_profile
|
|
544
|
+
{
|
|
545
|
+
profile_id: 'ruby-post-merge-validation',
|
|
546
|
+
phase: 'post_merge_validation',
|
|
547
|
+
separate_from: %w[merge_planning rendering],
|
|
548
|
+
checks: %w[
|
|
549
|
+
reparse_merged_output
|
|
550
|
+
resolved_owners_present
|
|
551
|
+
owner_count_not_unexpectedly_lower
|
|
552
|
+
unchanged_significant_lines_preserved
|
|
553
|
+
branch_added_significant_lines_preserved
|
|
554
|
+
output_length_within_policy_bounds
|
|
555
|
+
conflict_marker_shape_compatible
|
|
556
|
+
],
|
|
557
|
+
failure_outcomes: %w[fallback_to_baseline scoped_conflict hard_diagnostic_failure],
|
|
558
|
+
hooks: {
|
|
559
|
+
ci: 'strict',
|
|
560
|
+
exploratory: 'permissive_when_explicit'
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
def ruby_conflict_diagnostics_profile
|
|
566
|
+
{
|
|
567
|
+
profile_id: 'ruby-source-conflict-diagnostics',
|
|
568
|
+
conflict_kinds: %w[
|
|
569
|
+
both_modified
|
|
570
|
+
both_added
|
|
571
|
+
modify_delete
|
|
572
|
+
rename_rename
|
|
573
|
+
rename_modify
|
|
574
|
+
order_sensitive_sibling_additions
|
|
575
|
+
interstitial_conflict
|
|
576
|
+
validation_failure
|
|
577
|
+
],
|
|
578
|
+
risk_levels: %w[text_only syntax_level semantic_risk unknown],
|
|
579
|
+
marker_compatibility: {
|
|
580
|
+
standard_markers: true,
|
|
581
|
+
enhanced_metadata: 'sidecar_or_review_state'
|
|
582
|
+
},
|
|
583
|
+
audit_fields: %w[
|
|
584
|
+
owner_identity
|
|
585
|
+
owner_kind
|
|
586
|
+
strategy_chosen
|
|
587
|
+
match_confidence
|
|
588
|
+
fallback_reason
|
|
589
|
+
validation_warnings
|
|
590
|
+
conflict_kind
|
|
591
|
+
conflict_scope
|
|
592
|
+
],
|
|
593
|
+
stable_for_review_replay: true
|
|
594
|
+
}
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
def ruby_formatter_policy_profile
|
|
598
|
+
{
|
|
599
|
+
profile_id: 'ruby-source-formatter-policy',
|
|
600
|
+
adapter_phase: 'optional_post_merge_adapter',
|
|
601
|
+
semantic_validation: 'not_proven_by_formatter',
|
|
602
|
+
policies: %w[
|
|
603
|
+
no_formatter
|
|
604
|
+
validate_only
|
|
605
|
+
format_after_clean_merge
|
|
606
|
+
format_after_fallback
|
|
607
|
+
formatter_failure_is_warning
|
|
608
|
+
formatter_failure_is_hard_error
|
|
609
|
+
],
|
|
610
|
+
portable_fixture_default: 'no_formatter',
|
|
611
|
+
formatter_execution_in_portable_expectations: 'only_when_fixture_opts_in',
|
|
612
|
+
invariants: %w[owner_identity conflict_scope validation_semantics]
|
|
613
|
+
}
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
def ruby_formatter_adapter_report(pre_format_output:, formatted_output:, policy: 'validate_only',
|
|
617
|
+
conflict_scope: 'none')
|
|
618
|
+
pre_format_owners = ruby_source_owner_identity_profile(pre_format_output)
|
|
619
|
+
formatted_owners = ruby_source_owner_identity_profile(formatted_output)
|
|
620
|
+
formatted_owner_signatures = stable_owner_signatures(formatted_owners)
|
|
621
|
+
owners_preserved = stable_owner_signatures(pre_format_owners) == formatted_owner_signatures
|
|
622
|
+
whitespace_repaired = pre_format_output != formatted_output
|
|
623
|
+
|
|
624
|
+
{
|
|
625
|
+
policy: policy,
|
|
626
|
+
formatter_profile: ruby_formatter_policy_profile.fetch(:profile_id),
|
|
627
|
+
adapter_phase: 'optional_post_merge_adapter',
|
|
628
|
+
semantic_validation: 'not_proven_by_formatter',
|
|
629
|
+
whitespace_repaired: whitespace_repaired,
|
|
630
|
+
owners_preserved: owners_preserved,
|
|
631
|
+
conflict_scope_preserved: true,
|
|
632
|
+
validation_semantics_preserved: true,
|
|
633
|
+
conflict_scope: conflict_scope,
|
|
634
|
+
portable_expectation: 'formatter_not_executed_unless_fixture_opts_in',
|
|
635
|
+
owner_signatures: formatted_owner_signatures,
|
|
636
|
+
diagnostics: [
|
|
637
|
+
{
|
|
638
|
+
severity: owners_preserved ? 'info' : 'error',
|
|
639
|
+
category: owners_preserved ? 'formatter_adapter_accepted' : 'formatter_adapter_rejected',
|
|
640
|
+
message: owners_preserved ? 'Ruby formatter adapter preserved owner identity, conflict scope, and validation semantics.' : 'Ruby formatter adapter changed owner identity and cannot be accepted as a semantic merge.'
|
|
641
|
+
}
|
|
642
|
+
]
|
|
643
|
+
}
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
def ruby_ast_node_merge_strategy_profile
|
|
647
|
+
{
|
|
648
|
+
profile_id: 'ruby-optional-ast-node-merge',
|
|
649
|
+
merge_surfaces: %w[owner ast_node line hybrid],
|
|
650
|
+
optional_fine_grained_profiles: %w[expression argument_list hash_literal_pair],
|
|
651
|
+
child_ordering_strategies: %w[destination_order successor_constraints pcs_like_triples],
|
|
652
|
+
public_contract_level: 'ruleset_and_fixture',
|
|
653
|
+
default_surface: 'owner',
|
|
654
|
+
backend_strategy_choices: %w[entity_level ast_level line_level hybrid],
|
|
655
|
+
reconstruction_policy: {
|
|
656
|
+
preserve_original_text_unless_backend_declares_renderer: true,
|
|
657
|
+
conflict_marker_placement_requires_text_boundary: true,
|
|
658
|
+
risky_reconstruction_outcome: 'fallback_or_scoped_conflict'
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
def ruby_ast_node_merge_candidate_report(surface:, base:, template:, destination:, reconstruction_risk: false)
|
|
664
|
+
{
|
|
665
|
+
surface: surface,
|
|
666
|
+
strategy_profile: ruby_ast_node_merge_strategy_profile.fetch(:profile_id),
|
|
667
|
+
candidate_strategy: reconstruction_risk ? 'fallback_or_scoped_conflict' : 'hybrid_ast_node_merge',
|
|
668
|
+
owner_level_fallback_too_blunt: !reconstruction_risk,
|
|
669
|
+
successor_ordering_available: true,
|
|
670
|
+
pcs_like_strategy_available: true,
|
|
671
|
+
public_contract_level: 'ruleset_and_fixture',
|
|
672
|
+
backend_strategy_choices: ruby_ast_node_merge_strategy_profile.fetch(:backend_strategy_choices),
|
|
673
|
+
inputs: {
|
|
674
|
+
base: base,
|
|
675
|
+
template: template,
|
|
676
|
+
destination: destination
|
|
677
|
+
},
|
|
678
|
+
reconstruction: {
|
|
679
|
+
risky: reconstruction_risk,
|
|
680
|
+
outcome: reconstruction_risk ? 'fallback_or_scoped_conflict' : 'preserve_original_text_boundaries'
|
|
681
|
+
},
|
|
682
|
+
diagnostics: [
|
|
683
|
+
{
|
|
684
|
+
severity: reconstruction_risk ? 'warning' : 'info',
|
|
685
|
+
category: reconstruction_risk ? 'ast_node_reconstruction_risk' : 'ast_node_merge_candidate',
|
|
686
|
+
message: reconstruction_risk ? 'Ruby AST-node merge candidate has ambiguous whitespace, comment, or marker reconstruction boundaries.' : 'Ruby AST-node merge candidate can be considered when owner-level fallback would be too blunt.'
|
|
687
|
+
}
|
|
688
|
+
]
|
|
689
|
+
}
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
def ruby_vcs_tool_integration_profile
|
|
693
|
+
{
|
|
694
|
+
profile_id: 'ruby-vcs-tool-integration',
|
|
695
|
+
hosts: {
|
|
696
|
+
git_merge_driver: {
|
|
697
|
+
contract: 'git_merge_driver',
|
|
698
|
+
placeholders: %w[%O %A %B %P],
|
|
699
|
+
output_target: '%A',
|
|
700
|
+
standard_marker_modes: %w[diff3 zdiff3 merge],
|
|
701
|
+
marker_size: 'host_provided'
|
|
702
|
+
},
|
|
703
|
+
jujutsu_merge_tool: {
|
|
704
|
+
contract: 'jj_merge_tool',
|
|
705
|
+
roles: %w[base left right output path],
|
|
706
|
+
output_target: 'output',
|
|
707
|
+
standard_marker_modes: %w[diff3 merge],
|
|
708
|
+
marker_size: 'host_provided'
|
|
709
|
+
}
|
|
710
|
+
},
|
|
711
|
+
enhanced_markers: {
|
|
712
|
+
optional: true,
|
|
713
|
+
requires_host_tolerance: true,
|
|
714
|
+
default: 'standard_markers'
|
|
715
|
+
},
|
|
716
|
+
audit_artifact: {
|
|
717
|
+
enabled: true,
|
|
718
|
+
formats: %w[json],
|
|
719
|
+
fields: %w[host operation path fallback_reason validation_warnings conflict_kind timeout_ms]
|
|
720
|
+
},
|
|
721
|
+
resource_budget: {
|
|
722
|
+
timeout_ms: 5000,
|
|
723
|
+
timeout_outcome: 'fallback_or_driver_error',
|
|
724
|
+
cannot_hang_vcs_operation: true
|
|
725
|
+
},
|
|
726
|
+
diagnostics: %w[
|
|
727
|
+
structured_merge_skipped
|
|
728
|
+
fallback_activated
|
|
729
|
+
driver_invocation_error
|
|
730
|
+
tool_invocation_error
|
|
731
|
+
timeout_or_resource_budget
|
|
732
|
+
]
|
|
733
|
+
}
|
|
734
|
+
end
|
|
735
|
+
|
|
736
|
+
def ruby_vcs_tool_invocation_report(host:, event:, path:, timeout_ms: 5000)
|
|
737
|
+
severity = event.to_s.end_with?('error') ? 'error' : 'warning'
|
|
738
|
+
|
|
739
|
+
{
|
|
740
|
+
host: host,
|
|
741
|
+
event: event,
|
|
742
|
+
path: path,
|
|
743
|
+
integration_profile: ruby_vcs_tool_integration_profile.fetch(:profile_id),
|
|
744
|
+
marker_mode: 'standard_markers',
|
|
745
|
+
marker_size: 'host_provided',
|
|
746
|
+
audit_artifact: {
|
|
747
|
+
format: 'json',
|
|
748
|
+
required: true
|
|
749
|
+
},
|
|
750
|
+
timeout_ms: timeout_ms,
|
|
751
|
+
resource_budget_enforced: true,
|
|
752
|
+
diagnostics: [
|
|
753
|
+
{
|
|
754
|
+
severity: severity,
|
|
755
|
+
category: event,
|
|
756
|
+
message: "Ruby #{host} integration reported #{event} for #{path}."
|
|
757
|
+
}
|
|
758
|
+
]
|
|
759
|
+
}
|
|
760
|
+
end
|
|
761
|
+
|
|
762
|
+
def ruby_silent_data_loss_validation_report(template_source:, destination_source:, output:)
|
|
763
|
+
significant_inputs = {
|
|
764
|
+
template: significant_source_lines(template_source),
|
|
765
|
+
destination: significant_source_lines(destination_source)
|
|
766
|
+
}
|
|
767
|
+
output_lines = significant_source_lines(output).to_h { |line| [line, true] }
|
|
768
|
+
missing = significant_inputs.flat_map do |side, lines|
|
|
769
|
+
lines.reject { |line| output_lines[line] }.map do |line|
|
|
770
|
+
{
|
|
771
|
+
side: side.to_s,
|
|
772
|
+
line: line,
|
|
773
|
+
check: 'branch_added_significant_lines_preserved'
|
|
774
|
+
}
|
|
775
|
+
end
|
|
776
|
+
end
|
|
777
|
+
|
|
778
|
+
{
|
|
779
|
+
ok: missing.empty?,
|
|
780
|
+
validation_profile: ruby_post_merge_validation_profile.fetch(:profile_id),
|
|
781
|
+
failures: missing,
|
|
782
|
+
outcome: missing.empty? ? 'accepted' : 'hard_diagnostic_failure',
|
|
783
|
+
diagnostics: if missing.empty?
|
|
784
|
+
[]
|
|
785
|
+
else
|
|
786
|
+
[
|
|
787
|
+
{
|
|
788
|
+
severity: 'error',
|
|
789
|
+
category: 'silent_data_loss_prevention',
|
|
790
|
+
message: 'Ruby post-merge validation detected significant input lines missing from output.'
|
|
791
|
+
}
|
|
792
|
+
]
|
|
793
|
+
end
|
|
794
|
+
}
|
|
795
|
+
end
|
|
796
|
+
|
|
797
|
+
def ruby_fallback_scope_guard_report(requested_scope:, declared_scope:)
|
|
798
|
+
widened = ruby_fallback_scope_rank(requested_scope) > ruby_fallback_scope_rank(declared_scope)
|
|
799
|
+
{
|
|
800
|
+
requested_scope: requested_scope,
|
|
801
|
+
declared_scope: declared_scope,
|
|
802
|
+
widened: widened,
|
|
803
|
+
activated: !widened,
|
|
804
|
+
diagnostics: [
|
|
805
|
+
{
|
|
806
|
+
severity: widened ? 'error' : 'info',
|
|
807
|
+
category: widened ? 'fallback_scope_widening_rejected' : 'fallback_scope_accepted',
|
|
808
|
+
message: widened ? "Ruby fallback cannot widen from #{declared_scope} to #{requested_scope} without an explicit policy." : 'Ruby fallback scope is within the declared policy.'
|
|
809
|
+
}
|
|
810
|
+
]
|
|
811
|
+
}
|
|
812
|
+
end
|
|
813
|
+
|
|
814
|
+
def ruby_interstitial_merge_policy_profile
|
|
815
|
+
{
|
|
816
|
+
policy_id: 'ruby-source-interstitial-merge',
|
|
817
|
+
separates_owner_merge: true,
|
|
818
|
+
region_kinds: %w[file_header file_footer container_header container_footer between],
|
|
819
|
+
owner_adjacency_fields: %w[previous_owner next_owner],
|
|
820
|
+
rules: [
|
|
821
|
+
{
|
|
822
|
+
region_kind: 'require',
|
|
823
|
+
ordering: 'destination_order_then_template_additions',
|
|
824
|
+
duplicate_key: 'require_path'
|
|
825
|
+
},
|
|
826
|
+
{
|
|
827
|
+
region_kind: 'blank_line',
|
|
828
|
+
ownership: 'preserve_declared_region_owner'
|
|
829
|
+
},
|
|
830
|
+
{
|
|
831
|
+
region_kind: 'comment',
|
|
832
|
+
attachment: 'nearest_declared_owner_or_standalone'
|
|
833
|
+
}
|
|
834
|
+
]
|
|
835
|
+
}
|
|
836
|
+
end
|
|
837
|
+
|
|
838
|
+
def ruby_child_group_profile
|
|
839
|
+
{
|
|
840
|
+
profile_id: 'ruby-source-child-groups',
|
|
841
|
+
groups: [
|
|
842
|
+
{
|
|
843
|
+
owner_kind: 'class',
|
|
844
|
+
child_group: 'methods',
|
|
845
|
+
ordering: 'policy_ordered',
|
|
846
|
+
ordering_policy: DEFAULT_METHOD_MOVE_POLICY,
|
|
847
|
+
commutative: false,
|
|
848
|
+
visibility_sections: %w[public protected private]
|
|
849
|
+
},
|
|
850
|
+
{
|
|
851
|
+
owner_kind: 'class',
|
|
852
|
+
child_group: 'constants',
|
|
853
|
+
ordering: 'destination_order_then_template_additions',
|
|
854
|
+
commutative: false
|
|
855
|
+
},
|
|
856
|
+
{
|
|
857
|
+
owner_kind: 'module',
|
|
858
|
+
child_group: 'declarations',
|
|
859
|
+
ordering: 'destination_order_then_template_additions',
|
|
860
|
+
commutative: false
|
|
861
|
+
}
|
|
862
|
+
],
|
|
863
|
+
diagnostics: [
|
|
864
|
+
{
|
|
865
|
+
severity: 'info',
|
|
866
|
+
category: 'ruby_child_group_profile',
|
|
867
|
+
message: 'Ruby child groups preserve destination order unless an explicit policy says otherwise.'
|
|
868
|
+
}
|
|
869
|
+
]
|
|
870
|
+
}
|
|
871
|
+
end
|
|
872
|
+
|
|
873
|
+
def ruby_interstitial_comment_attachment_report(source)
|
|
874
|
+
lines = normalize_source(source).lines(chomp: true)
|
|
875
|
+
owners = top_level_source_region_owners(lines)
|
|
876
|
+
source_comment_block_attachment_report(
|
|
877
|
+
lines: lines,
|
|
878
|
+
owners: owners,
|
|
879
|
+
comment_line: method(:comment_line?)
|
|
880
|
+
)
|
|
881
|
+
end
|
|
882
|
+
|
|
883
|
+
def ruby_blank_line_ownership_report(source)
|
|
884
|
+
regions = ruby_source_regions(source)[:regions]
|
|
885
|
+
{
|
|
886
|
+
blank_line_regions: source_blank_line_ownership_regions(regions: regions)
|
|
887
|
+
}
|
|
888
|
+
end
|
|
889
|
+
|
|
890
|
+
def ruby_rename_detection_policy_profile
|
|
891
|
+
{
|
|
892
|
+
policy_id: 'ruby-source-rename-detection',
|
|
893
|
+
capability: {
|
|
894
|
+
name: 'rename_detection',
|
|
895
|
+
enabled: true,
|
|
896
|
+
default_enabled: false,
|
|
897
|
+
explicit: true
|
|
898
|
+
},
|
|
899
|
+
signals: %w[body_hash_with_owner_name_normalization structural_hash token_similarity parent_scope_similarity
|
|
900
|
+
backend_native_move_metadata],
|
|
901
|
+
clean_rename_confidence: 'content_hash',
|
|
902
|
+
conflict_policy: 'report_rename_plus_edit'
|
|
903
|
+
}
|
|
904
|
+
end
|
|
905
|
+
|
|
906
|
+
def ruby_rename_detection(template_source, destination_source)
|
|
907
|
+
template_methods = ruby_method_identity_entries(template_source)
|
|
908
|
+
destination_methods = ruby_method_identity_entries(destination_source)
|
|
909
|
+
destination_by_parent_and_body = destination_methods.group_by do |entry|
|
|
910
|
+
[entry[:parent_scope], entry[:normalized_body_identity]]
|
|
911
|
+
end
|
|
912
|
+
destination_signature_keys = destination_methods.to_h do |entry|
|
|
913
|
+
[[entry[:parent_scope], entry[:signature]], true]
|
|
914
|
+
end
|
|
915
|
+
matched_destination_addresses = {}
|
|
916
|
+
|
|
917
|
+
renames = template_methods.filter_map do |template_entry|
|
|
918
|
+
next if destination_signature_keys[[template_entry[:parent_scope], template_entry[:signature]]]
|
|
919
|
+
|
|
920
|
+
destination_entry = destination_by_parent_and_body.fetch(
|
|
921
|
+
[template_entry[:parent_scope], template_entry[:normalized_body_identity]],
|
|
922
|
+
[]
|
|
923
|
+
).find { |entry| entry[:signature] != template_entry[:signature] }
|
|
924
|
+
next unless destination_entry
|
|
925
|
+
|
|
926
|
+
matched_destination_addresses[destination_entry[:address]] = true
|
|
927
|
+
{
|
|
928
|
+
from_address: template_entry[:address],
|
|
929
|
+
to_address: destination_entry[:address],
|
|
930
|
+
from_name: template_entry[:signature],
|
|
931
|
+
to_name: destination_entry[:signature],
|
|
932
|
+
parent_scope: template_entry[:parent_scope],
|
|
933
|
+
confidence: 'content_hash',
|
|
934
|
+
signals: %w[body_hash_with_owner_name_normalization parent_scope_similarity],
|
|
935
|
+
clean_rename: true
|
|
936
|
+
}
|
|
937
|
+
end
|
|
938
|
+
|
|
939
|
+
{
|
|
940
|
+
policy: ruby_rename_detection_policy_profile,
|
|
941
|
+
renames: renames,
|
|
942
|
+
diagnostics: if renames.empty?
|
|
943
|
+
[]
|
|
944
|
+
else
|
|
945
|
+
[
|
|
946
|
+
{
|
|
947
|
+
severity: 'info',
|
|
948
|
+
category: 'ruby_rename_detection',
|
|
949
|
+
message: 'Ruby rename detection is explicit and reports clean same-parent method renames by normalized body hash.'
|
|
950
|
+
}
|
|
951
|
+
]
|
|
952
|
+
end,
|
|
953
|
+
unmatched_destination: destination_methods.reject do |entry|
|
|
954
|
+
matched_destination_addresses[entry[:address]]
|
|
955
|
+
end.map { |entry| entry[:address] }
|
|
956
|
+
}
|
|
957
|
+
end
|
|
958
|
+
|
|
959
|
+
def ruby_rename_plus_edit_conflicts(base_source, template_source, destination_source)
|
|
960
|
+
base_methods = ruby_method_identity_entries(base_source)
|
|
961
|
+
template_methods = ruby_method_identity_entries(template_source)
|
|
962
|
+
destination_methods = ruby_method_identity_entries(destination_source)
|
|
963
|
+
template_by_parent = template_methods.group_by { |entry| entry[:parent_scope] }
|
|
964
|
+
destination_by_parent = destination_methods.group_by { |entry| entry[:parent_scope] }
|
|
965
|
+
|
|
966
|
+
conflicts = base_methods.filter_map do |base_entry|
|
|
967
|
+
template_candidates = template_by_parent.fetch(base_entry[:parent_scope], []).reject do |entry|
|
|
968
|
+
entry[:signature] == base_entry[:signature]
|
|
969
|
+
end
|
|
970
|
+
destination_candidates = destination_by_parent.fetch(base_entry[:parent_scope], []).reject do |entry|
|
|
971
|
+
entry[:signature] == base_entry[:signature]
|
|
972
|
+
end
|
|
973
|
+
next if template_candidates.empty? || destination_candidates.empty?
|
|
974
|
+
|
|
975
|
+
template_candidate = template_candidates.first
|
|
976
|
+
destination_candidate = destination_candidates.first
|
|
977
|
+
next if template_candidate[:signature] == destination_candidate[:signature]
|
|
978
|
+
|
|
979
|
+
{
|
|
980
|
+
base_address: base_entry[:address],
|
|
981
|
+
template_address: template_candidate[:address],
|
|
982
|
+
destination_address: destination_candidate[:address],
|
|
983
|
+
parent_scope: base_entry[:parent_scope],
|
|
984
|
+
conflict_kind: 'rename_plus_edit',
|
|
985
|
+
fallback_scope: 'owned_region',
|
|
986
|
+
confidence: 'unresolved',
|
|
987
|
+
diagnostics: [
|
|
988
|
+
'both branches renamed the same Ruby owner differently',
|
|
989
|
+
'method body identity changed on at least one side'
|
|
990
|
+
]
|
|
991
|
+
}
|
|
992
|
+
end
|
|
993
|
+
|
|
994
|
+
{
|
|
995
|
+
policy: ruby_rename_detection_policy_profile,
|
|
996
|
+
conflicts: conflicts,
|
|
997
|
+
diagnostics: if conflicts.empty?
|
|
998
|
+
[]
|
|
999
|
+
else
|
|
1000
|
+
[
|
|
1001
|
+
{
|
|
1002
|
+
severity: 'warning',
|
|
1003
|
+
category: 'ruby_rename_plus_edit_conflict',
|
|
1004
|
+
message: 'Ruby rename detection found incompatible rename-plus-edit changes.'
|
|
1005
|
+
}
|
|
1006
|
+
]
|
|
1007
|
+
end
|
|
1008
|
+
}
|
|
1009
|
+
end
|
|
1010
|
+
|
|
1011
|
+
def ruby_cross_container_method_move_detection(template_source, destination_source)
|
|
1012
|
+
template_methods = ruby_method_identity_entries(template_source)
|
|
1013
|
+
destination_methods = ruby_method_identity_entries(destination_source)
|
|
1014
|
+
destination_by_signature_and_body = destination_methods.group_by do |entry|
|
|
1015
|
+
[entry[:signature], entry[:normalized_body_identity]]
|
|
1016
|
+
end
|
|
1017
|
+
|
|
1018
|
+
moves = template_methods.filter_map do |template_entry|
|
|
1019
|
+
destination_entry = destination_by_signature_and_body.fetch(
|
|
1020
|
+
[template_entry[:signature], template_entry[:normalized_body_identity]],
|
|
1021
|
+
[]
|
|
1022
|
+
).find { |entry| entry[:parent_scope] != template_entry[:parent_scope] }
|
|
1023
|
+
next unless destination_entry
|
|
1024
|
+
|
|
1025
|
+
{
|
|
1026
|
+
from_address: template_entry[:address],
|
|
1027
|
+
to_address: destination_entry[:address],
|
|
1028
|
+
from_parent_scope: template_entry[:parent_scope],
|
|
1029
|
+
to_parent_scope: destination_entry[:parent_scope],
|
|
1030
|
+
signature: template_entry[:signature],
|
|
1031
|
+
moved: true,
|
|
1032
|
+
move_kind: 'cross_container',
|
|
1033
|
+
ordering_policy: DEFAULT_METHOD_MOVE_POLICY,
|
|
1034
|
+
preserves_destination_order: true,
|
|
1035
|
+
confidence: 'content_hash'
|
|
1036
|
+
}
|
|
1037
|
+
end
|
|
1038
|
+
|
|
1039
|
+
{
|
|
1040
|
+
capability: {
|
|
1041
|
+
name: 'move_detection',
|
|
1042
|
+
enabled: true,
|
|
1043
|
+
default_enabled: false,
|
|
1044
|
+
requires_stable_node_identity: true
|
|
1045
|
+
},
|
|
1046
|
+
moves: moves,
|
|
1047
|
+
diagnostics: if moves.empty?
|
|
1048
|
+
[]
|
|
1049
|
+
else
|
|
1050
|
+
[
|
|
1051
|
+
{
|
|
1052
|
+
severity: 'info',
|
|
1053
|
+
category: 'ruby_cross_container_method_move',
|
|
1054
|
+
message: 'Ruby detected same-signature method movement across containers while preserving destination order.'
|
|
1055
|
+
}
|
|
1056
|
+
]
|
|
1057
|
+
end
|
|
1058
|
+
}
|
|
1059
|
+
end
|
|
1060
|
+
|
|
176
1061
|
def apply_ruby_delegated_child_outputs(source, delegated_operations, apply_plan, applied_children)
|
|
177
1062
|
lines = normalize_source(source).split("\n")
|
|
178
1063
|
operations_by_id = delegated_operations.to_h { |operation| [operation[:operation_id], operation] }
|
|
@@ -189,14 +1074,20 @@ module Ruby
|
|
|
189
1074
|
|
|
190
1075
|
replacements.sort_by { |entry| -entry[:start] }.each do |entry|
|
|
191
1076
|
prefix = comment_prefix_for(lines[entry[:start]])
|
|
192
|
-
replacement_lines = entry[:output].empty?
|
|
1077
|
+
replacement_lines = if entry[:output].empty?
|
|
1078
|
+
[]
|
|
1079
|
+
else
|
|
1080
|
+
entry[:output].sub(/\n\z/, '').split("\n").map do |line|
|
|
1081
|
+
"#{prefix}#{line}"
|
|
1082
|
+
end
|
|
1083
|
+
end
|
|
193
1084
|
lines[entry[:start]..entry[:finish]] = replacement_lines
|
|
194
1085
|
end
|
|
195
1086
|
|
|
196
1087
|
{
|
|
197
1088
|
ok: true,
|
|
198
1089
|
diagnostics: [],
|
|
199
|
-
output: "#{lines.join("\n").sub(/\n+\z/,
|
|
1090
|
+
output: "#{lines.join("\n").sub(/\n+\z/, '')}\n",
|
|
200
1091
|
policies: [DESTINATION_WINS_ARRAY_POLICY]
|
|
201
1092
|
}
|
|
202
1093
|
end
|
|
@@ -204,8 +1095,8 @@ module Ruby
|
|
|
204
1095
|
def merge_ruby_with_nested_outputs(template_source, destination_source, dialect, nested_outputs)
|
|
205
1096
|
Ast::Merge.execute_nested_merge(
|
|
206
1097
|
nested_outputs,
|
|
207
|
-
default_family:
|
|
208
|
-
request_id_prefix:
|
|
1098
|
+
default_family: 'ruby',
|
|
1099
|
+
request_id_prefix: 'nested_ruby_child',
|
|
209
1100
|
merge_parent: -> { merge_ruby(template_source, destination_source, dialect) },
|
|
210
1101
|
discover_operations: lambda { |merged_output|
|
|
211
1102
|
analysis = parse_ruby(merged_output, dialect)
|
|
@@ -228,10 +1119,11 @@ module Ruby
|
|
|
228
1119
|
)
|
|
229
1120
|
end
|
|
230
1121
|
|
|
231
|
-
def merge_ruby_with_reviewed_nested_outputs(template_source, destination_source, dialect, review_state,
|
|
1122
|
+
def merge_ruby_with_reviewed_nested_outputs(template_source, destination_source, dialect, review_state,
|
|
1123
|
+
applied_children)
|
|
232
1124
|
Ast::Merge.execute_reviewed_nested_merge(
|
|
233
1125
|
review_state,
|
|
234
|
-
|
|
1126
|
+
'ruby',
|
|
235
1127
|
applied_children,
|
|
236
1128
|
merge_parent: -> { merge_ruby(template_source, destination_source, dialect) },
|
|
237
1129
|
discover_operations: lambda { |merged_output|
|
|
@@ -255,9 +1147,13 @@ module Ruby
|
|
|
255
1147
|
)
|
|
256
1148
|
end
|
|
257
1149
|
|
|
258
|
-
def merge_ruby_with_reviewed_nested_outputs_from_replay_bundle(template_source, destination_source, dialect,
|
|
259
|
-
|
|
260
|
-
|
|
1150
|
+
def merge_ruby_with_reviewed_nested_outputs_from_replay_bundle(template_source, destination_source, dialect,
|
|
1151
|
+
replay_bundle)
|
|
1152
|
+
execution = Array(replay_bundle[:reviewed_nested_executions]).find { |entry| entry[:family] == 'ruby' }
|
|
1153
|
+
unless execution
|
|
1154
|
+
return { ok: false,
|
|
1155
|
+
diagnostics: [{ severity: 'error', category: 'configuration_error', message: 'review replay bundle does not include a reviewed nested execution for ruby.' }], policies: [] }
|
|
1156
|
+
end
|
|
261
1157
|
|
|
262
1158
|
merge_ruby_with_reviewed_nested_outputs(
|
|
263
1159
|
template_source,
|
|
@@ -268,9 +1164,13 @@ module Ruby
|
|
|
268
1164
|
)
|
|
269
1165
|
end
|
|
270
1166
|
|
|
271
|
-
def merge_ruby_with_reviewed_nested_outputs_from_review_state(template_source, destination_source, dialect,
|
|
272
|
-
|
|
273
|
-
|
|
1167
|
+
def merge_ruby_with_reviewed_nested_outputs_from_review_state(template_source, destination_source, dialect,
|
|
1168
|
+
review_state)
|
|
1169
|
+
execution = Array(review_state[:reviewed_nested_executions]).find { |entry| entry[:family] == 'ruby' }
|
|
1170
|
+
unless execution
|
|
1171
|
+
return { ok: false,
|
|
1172
|
+
diagnostics: [{ severity: 'error', category: 'configuration_error', message: 'review state does not include a reviewed nested execution for ruby.' }], policies: [] }
|
|
1173
|
+
end
|
|
274
1174
|
|
|
275
1175
|
merge_ruby_with_reviewed_nested_outputs(
|
|
276
1176
|
template_source,
|
|
@@ -281,9 +1181,13 @@ module Ruby
|
|
|
281
1181
|
)
|
|
282
1182
|
end
|
|
283
1183
|
|
|
284
|
-
def merge_ruby_with_reviewed_nested_outputs_from_replay_bundle_envelope(template_source, destination_source,
|
|
1184
|
+
def merge_ruby_with_reviewed_nested_outputs_from_replay_bundle_envelope(template_source, destination_source,
|
|
1185
|
+
dialect, envelope)
|
|
285
1186
|
replay_bundle, import_error = Ast::Merge.import_review_replay_bundle_envelope(envelope)
|
|
286
|
-
|
|
1187
|
+
if import_error
|
|
1188
|
+
return { ok: false,
|
|
1189
|
+
diagnostics: [{ severity: 'error', category: import_error[:category], message: import_error[:message] }], policies: [] }
|
|
1190
|
+
end
|
|
287
1191
|
|
|
288
1192
|
merge_ruby_with_reviewed_nested_outputs_from_replay_bundle(
|
|
289
1193
|
template_source,
|
|
@@ -293,9 +1197,13 @@ module Ruby
|
|
|
293
1197
|
)
|
|
294
1198
|
end
|
|
295
1199
|
|
|
296
|
-
def merge_ruby_with_reviewed_nested_outputs_from_review_state_envelope(template_source, destination_source,
|
|
1200
|
+
def merge_ruby_with_reviewed_nested_outputs_from_review_state_envelope(template_source, destination_source,
|
|
1201
|
+
dialect, envelope)
|
|
297
1202
|
review_state, import_error = Ast::Merge.import_conformance_manifest_review_state_envelope(envelope)
|
|
298
|
-
|
|
1203
|
+
if import_error
|
|
1204
|
+
return { ok: false,
|
|
1205
|
+
diagnostics: [{ severity: 'error', category: import_error[:category], message: import_error[:message] }], policies: [] }
|
|
1206
|
+
end
|
|
299
1207
|
|
|
300
1208
|
merge_ruby_with_reviewed_nested_outputs_from_review_state(
|
|
301
1209
|
template_source,
|
|
@@ -305,10 +1213,9 @@ module Ruby
|
|
|
305
1213
|
)
|
|
306
1214
|
end
|
|
307
1215
|
|
|
308
|
-
def analyze_ruby_document(source)
|
|
1216
|
+
def analyze_ruby_document(source, process_analysis: nil)
|
|
309
1217
|
lines = normalize_source(source).split("\n", -1)
|
|
310
|
-
requires =
|
|
311
|
-
declarations = []
|
|
1218
|
+
requires = ruby_analysis_require_owners(source, process_analysis)
|
|
312
1219
|
discovered_surfaces = []
|
|
313
1220
|
pending_comments = []
|
|
314
1221
|
|
|
@@ -326,23 +1233,15 @@ module Ruby
|
|
|
326
1233
|
next
|
|
327
1234
|
end
|
|
328
1235
|
|
|
329
|
-
if (
|
|
330
|
-
|
|
331
|
-
path: "/requires/#{requires.length}",
|
|
332
|
-
owner_kind: "require",
|
|
333
|
-
match_key: match[1]
|
|
334
|
-
}
|
|
1236
|
+
if ruby_process_import_item_at_line(process_analysis,
|
|
1237
|
+
line_number) || legacy_require_line?(line, process_analysis)
|
|
335
1238
|
pending_comments = []
|
|
336
1239
|
next
|
|
337
1240
|
end
|
|
338
1241
|
|
|
339
|
-
declaration =
|
|
1242
|
+
declaration = ruby_process_structure_item_at_line(process_analysis, line_number)
|
|
1243
|
+
declaration ||= declaration_for_line(line) if Array(process_analysis&.structure).empty?
|
|
340
1244
|
if declaration
|
|
341
|
-
declarations << {
|
|
342
|
-
path: "/declarations/#{declaration[:name]}",
|
|
343
|
-
owner_kind: "declaration",
|
|
344
|
-
match_key: declaration[:name]
|
|
345
|
-
}
|
|
346
1245
|
surfaces = surfaces_for_owner(
|
|
347
1246
|
owner_name: declaration[:name],
|
|
348
1247
|
comment_entries: pending_comments
|
|
@@ -355,25 +1254,29 @@ module Ruby
|
|
|
355
1254
|
pending_comments = []
|
|
356
1255
|
end
|
|
357
1256
|
|
|
1257
|
+
declaration_entries = ruby_process_owner_entries(process_analysis)
|
|
1258
|
+
declaration_entries = legacy_ruby_analysis_owner_entries(source) if declaration_entries.empty?
|
|
1259
|
+
declarations = declaration_entries.map do |entry|
|
|
1260
|
+
{
|
|
1261
|
+
path: entry[:path],
|
|
1262
|
+
owner_kind: 'declaration',
|
|
1263
|
+
match_key: entry[:name]
|
|
1264
|
+
}
|
|
1265
|
+
end
|
|
1266
|
+
|
|
358
1267
|
{
|
|
359
|
-
kind:
|
|
360
|
-
dialect:
|
|
361
|
-
root_kind:
|
|
1268
|
+
kind: 'ruby',
|
|
1269
|
+
dialect: 'ruby',
|
|
1270
|
+
root_kind: 'document',
|
|
362
1271
|
source: normalize_source(source),
|
|
1272
|
+
tree_haver_process_analysis: process_analysis,
|
|
363
1273
|
owners: (requires + declarations).sort_by { |owner| owner[:path] },
|
|
364
|
-
discovered_surfaces: discovered_surfaces
|
|
1274
|
+
discovered_surfaces: discovered_surfaces,
|
|
1275
|
+
method_shadowing: ruby_method_shadowing(source),
|
|
1276
|
+
diagnostics: ruby_method_shadowing_diagnostics(source)
|
|
365
1277
|
}
|
|
366
1278
|
end
|
|
367
1279
|
|
|
368
|
-
def collect_ruby_require_entries(source)
|
|
369
|
-
normalize_source(source).split("\n").filter_map do |line|
|
|
370
|
-
match = REQUIRE_PATTERN.match(line)
|
|
371
|
-
next unless match
|
|
372
|
-
|
|
373
|
-
{ path: "/requires/#{match[1]}", text: line.rstrip }
|
|
374
|
-
end
|
|
375
|
-
end
|
|
376
|
-
|
|
377
1280
|
def collect_ruby_preamble(source)
|
|
378
1281
|
lines = normalize_source(source).split("\n")
|
|
379
1282
|
preamble = []
|
|
@@ -385,98 +1288,1361 @@ module Ruby
|
|
|
385
1288
|
preamble.join("\n").strip
|
|
386
1289
|
end
|
|
387
1290
|
|
|
388
|
-
def
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
1291
|
+
def ruby_file_footer_text(source)
|
|
1292
|
+
regions = ruby_source_regions(source).fetch(:regions)
|
|
1293
|
+
footer = regions.reverse.find do |region|
|
|
1294
|
+
region[:region_kind] == 'interstitial' && region[:position] == 'file_footer'
|
|
1295
|
+
end
|
|
1296
|
+
content = footer.to_h.fetch(:content, '').strip
|
|
1297
|
+
return '' if content.empty?
|
|
393
1298
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
1299
|
+
content.lines.any? { |line| !line.strip.empty? && !comment_line?(line) } ? '' : content
|
|
1300
|
+
end
|
|
1301
|
+
|
|
1302
|
+
def collect_parse_errors(node)
|
|
1303
|
+
raise TreeHaver::NotAvailable, 'Ruby parse returned no root node' unless node
|
|
1304
|
+
return unless node.respond_to?(:has_error?) && node.has_error?
|
|
1305
|
+
|
|
1306
|
+
raise TreeHaver::NotAvailable,
|
|
1307
|
+
'Ruby parse contains syntax errors'
|
|
1308
|
+
end
|
|
1309
|
+
|
|
1310
|
+
def parse_failure_result(error)
|
|
1311
|
+
{
|
|
1312
|
+
ok: false,
|
|
1313
|
+
diagnostics: [{ severity: 'error', category: 'parse_error', message: error.message }],
|
|
1314
|
+
policies: []
|
|
1315
|
+
}
|
|
1316
|
+
end
|
|
1317
|
+
|
|
1318
|
+
def ruby_process_analysis_from_tree(source, root_node)
|
|
1319
|
+
structure = []
|
|
1320
|
+
imports = []
|
|
1321
|
+
|
|
1322
|
+
ruby_named_children(root_node).each do |node|
|
|
1323
|
+
case node.type
|
|
1324
|
+
when 'call'
|
|
1325
|
+
import = ruby_import_item_from_node(source, node)
|
|
1326
|
+
imports << import if import
|
|
1327
|
+
when 'if_modifier'
|
|
1328
|
+
import = ruby_import_item_from_modifier_node(source, node)
|
|
1329
|
+
imports << import if import
|
|
1330
|
+
when 'begin'
|
|
1331
|
+
import = ruby_import_item_from_begin_node(source, node)
|
|
1332
|
+
imports << import if import
|
|
1333
|
+
when 'class', 'module', 'method', 'singleton_method'
|
|
1334
|
+
item = ruby_structure_item_from_node(source, node)
|
|
1335
|
+
structure << item if item
|
|
1336
|
+
end
|
|
1337
|
+
end
|
|
1338
|
+
|
|
1339
|
+
TslpProcessAnalysis.new(structure: structure, imports: imports)
|
|
1340
|
+
end
|
|
1341
|
+
|
|
1342
|
+
def ruby_import_item_from_node(source, node)
|
|
1343
|
+
children = ruby_named_children(node)
|
|
1344
|
+
callee = children.first
|
|
1345
|
+
return unless callee && %w[identifier method_identifier].include?(callee.type)
|
|
1346
|
+
|
|
1347
|
+
name = ruby_node_text(source, callee)
|
|
1348
|
+
return unless %w[require require_relative].include?(name)
|
|
1349
|
+
|
|
1350
|
+
string_node = ruby_first_descendant(node) do |child|
|
|
1351
|
+
%w[string_content simple_symbol].include?(child.type)
|
|
1352
|
+
end
|
|
1353
|
+
return unless string_node
|
|
1354
|
+
|
|
1355
|
+
TslpImportItem.new(source: ruby_node_text(source, string_node), span: ruby_import_span_for(source, node))
|
|
1356
|
+
end
|
|
1357
|
+
|
|
1358
|
+
def ruby_import_item_from_modifier_node(source, node)
|
|
1359
|
+
call_node = ruby_named_children(node).first
|
|
1360
|
+
return unless call_node&.type == 'call'
|
|
1361
|
+
|
|
1362
|
+
import = ruby_import_item_from_node(source, call_node)
|
|
1363
|
+
return unless import
|
|
1364
|
+
|
|
1365
|
+
TslpImportItem.new(source: import.source, span: ruby_import_span_for(source, node))
|
|
1366
|
+
end
|
|
1367
|
+
|
|
1368
|
+
def ruby_import_item_from_begin_node(source, node)
|
|
1369
|
+
children = ruby_named_children(node)
|
|
1370
|
+
call_imports = children.filter_map do |child|
|
|
1371
|
+
ruby_import_item_from_node(source, child) if child.type == 'call'
|
|
1372
|
+
end
|
|
1373
|
+
return if call_imports.empty?
|
|
1374
|
+
|
|
1375
|
+
unsupported = children.any? do |child|
|
|
1376
|
+
child.type != 'call' && !ruby_load_error_rescue_node?(source, child)
|
|
1377
|
+
end
|
|
1378
|
+
return if unsupported
|
|
1379
|
+
|
|
1380
|
+
TslpImportItem.new(source: call_imports.map(&:source).join(','), span: ruby_span_for(node))
|
|
1381
|
+
end
|
|
1382
|
+
|
|
1383
|
+
def ruby_load_error_rescue_node?(source, node)
|
|
1384
|
+
return false unless node.type == 'rescue'
|
|
1385
|
+
return false unless ruby_named_children(node).all? { |child| child.type == 'exceptions' }
|
|
1386
|
+
|
|
1387
|
+
ruby_node_text(source, node).match?(/\Arescue\s+LoadError\b/)
|
|
1388
|
+
end
|
|
1389
|
+
|
|
1390
|
+
def ruby_structure_item_from_node(source, node)
|
|
1391
|
+
kind = case node.type
|
|
1392
|
+
when 'class'
|
|
1393
|
+
'class'
|
|
1394
|
+
when 'module'
|
|
1395
|
+
'module'
|
|
1396
|
+
when 'method', 'singleton_method'
|
|
1397
|
+
'method'
|
|
1398
|
+
end
|
|
1399
|
+
return unless kind
|
|
1400
|
+
|
|
1401
|
+
name_node = ruby_declaration_name_node(node)
|
|
1402
|
+
return unless name_node
|
|
1403
|
+
|
|
1404
|
+
TslpStructureItem.new(kind: kind, name: ruby_node_text(source, name_node), span: ruby_span_for(node))
|
|
1405
|
+
end
|
|
1406
|
+
|
|
1407
|
+
def ruby_declaration_name_node(node)
|
|
1408
|
+
case node.type
|
|
1409
|
+
when 'class', 'module'
|
|
1410
|
+
ruby_named_children(node).find do |child|
|
|
1411
|
+
%w[constant scope_resolution].include?(child.type)
|
|
1412
|
+
end
|
|
1413
|
+
when 'method'
|
|
1414
|
+
ruby_named_children(node).find do |child|
|
|
1415
|
+
%w[identifier method_identifier operator].include?(child.type)
|
|
1416
|
+
end
|
|
1417
|
+
when 'singleton_method'
|
|
1418
|
+
children = ruby_named_children(node)
|
|
1419
|
+
children.reverse.find do |child|
|
|
1420
|
+
%w[identifier method_identifier operator].include?(child.type)
|
|
1421
|
+
end
|
|
1422
|
+
end
|
|
1423
|
+
end
|
|
1424
|
+
|
|
1425
|
+
def ruby_first_descendant(node, &block)
|
|
1426
|
+
ruby_named_children(node).each do |child|
|
|
1427
|
+
return child if yield(child)
|
|
1428
|
+
|
|
1429
|
+
descendant = ruby_first_descendant(child, &block)
|
|
1430
|
+
return descendant if descendant
|
|
1431
|
+
end
|
|
1432
|
+
nil
|
|
1433
|
+
end
|
|
1434
|
+
|
|
1435
|
+
def ruby_named_children(node)
|
|
1436
|
+
node.children.select { |child| !child.respond_to?(:named?) || child.named? }
|
|
1437
|
+
end
|
|
1438
|
+
|
|
1439
|
+
def ruby_span_for(node)
|
|
1440
|
+
start_point = node.start_point
|
|
1441
|
+
end_point = node.end_point
|
|
1442
|
+
TslpSpan.new(
|
|
1443
|
+
start_row: start_point.fetch(:row),
|
|
1444
|
+
start_col: start_point.fetch(:column),
|
|
1445
|
+
end_row: end_point.fetch(:row),
|
|
1446
|
+
end_col: end_point.fetch(:column)
|
|
1447
|
+
)
|
|
1448
|
+
end
|
|
1449
|
+
|
|
1450
|
+
def ruby_import_span_for(source, node)
|
|
1451
|
+
span = ruby_span_for(node)
|
|
1452
|
+
lines = normalize_source(source).split("\n")
|
|
1453
|
+
start_row = span.start_row
|
|
1454
|
+
end_row = span.end_row
|
|
1455
|
+
|
|
1456
|
+
start_row -= 1 while start_row.positive? && coverage_directive_comment_line?(lines[start_row - 1])
|
|
1457
|
+
end_row += 1 while end_row < lines.length - 1 && coverage_directive_comment_line?(lines[end_row + 1])
|
|
1458
|
+
|
|
1459
|
+
TslpSpan.new(
|
|
1460
|
+
start_row: start_row,
|
|
1461
|
+
start_col: start_row == span.start_row ? span.start_col : 0,
|
|
1462
|
+
end_row: end_row,
|
|
1463
|
+
end_col: end_row == span.end_row ? span.end_col : lines[end_row].to_s.length
|
|
1464
|
+
)
|
|
1465
|
+
end
|
|
1466
|
+
|
|
1467
|
+
def coverage_directive_comment_line?(line)
|
|
1468
|
+
BlockDirectiveDetector.coverage_directive_line?(line)
|
|
1469
|
+
end
|
|
1470
|
+
|
|
1471
|
+
def ruby_node_text(source, node)
|
|
1472
|
+
source[node.start_byte...node.end_byte].to_s
|
|
1473
|
+
end
|
|
1474
|
+
|
|
1475
|
+
def ruby_fallback_scope_rank(scope)
|
|
1476
|
+
ruby_fallback_policy_profile.fetch(:scopes).index(scope.to_s) || Float::INFINITY
|
|
1477
|
+
end
|
|
1478
|
+
|
|
1479
|
+
def stable_owner_signatures(owner_identities)
|
|
1480
|
+
owner_identities.map do |identity|
|
|
1481
|
+
{
|
|
1482
|
+
owner_kind: identity.fetch(:owner_kind),
|
|
1483
|
+
owner_name: identity.fetch(:owner_name),
|
|
1484
|
+
parent_scope: identity.fetch(:parent_scope),
|
|
1485
|
+
structural_identity: identity.fetch(:structural_identity),
|
|
1486
|
+
occurrence_index: identity.fetch(:occurrence_index)
|
|
1487
|
+
}
|
|
1488
|
+
end
|
|
1489
|
+
end
|
|
1490
|
+
|
|
1491
|
+
def significant_source_lines(source)
|
|
1492
|
+
normalize_source(source).lines.map(&:strip).reject do |line|
|
|
1493
|
+
line.empty? || line.start_with?('#') || line == 'end'
|
|
1494
|
+
end
|
|
1495
|
+
end
|
|
1496
|
+
|
|
1497
|
+
def merge_ruby_requires(destination_requires, template_requires)
|
|
1498
|
+
destination_paths = destination_requires.to_h { |entry| [entry[:path], true] }
|
|
1499
|
+
destination_requires + template_requires.reject { |entry| destination_paths[entry[:path]] }
|
|
1500
|
+
end
|
|
1501
|
+
|
|
1502
|
+
def ruby_tslp_merge_context(analysis, role:)
|
|
1503
|
+
source = analysis.fetch(:source)
|
|
1504
|
+
process_analysis = analysis[:tree_haver_process_analysis]
|
|
1505
|
+
unsupported_lines = ruby_tslp_unsupported_top_level_lines(source, process_analysis)
|
|
1506
|
+
unless unsupported_lines.empty?
|
|
1507
|
+
return unsupported_feature_result(
|
|
1508
|
+
"ruby-merge can only merge TSLP-record-backed top-level Ruby declarations and imports; #{role} has unsupported top-level content on line(s) #{unsupported_lines.join(', ')}. Use prism-merge for native Ruby merging, or report missing Ruby process records to tree-sitter-language-pack."
|
|
1509
|
+
)
|
|
1510
|
+
end
|
|
1511
|
+
|
|
1512
|
+
{
|
|
1513
|
+
ok: true,
|
|
1514
|
+
source: source,
|
|
1515
|
+
preamble: ruby_tslp_file_preamble_text(source, process_analysis),
|
|
1516
|
+
requires: ruby_process_import_entries(source, process_analysis),
|
|
1517
|
+
declarations: collect_ruby_declaration_entries(source, process_analysis: process_analysis),
|
|
1518
|
+
footer: ruby_tslp_file_footer_text(source, process_analysis)
|
|
1519
|
+
}
|
|
1520
|
+
end
|
|
1521
|
+
|
|
1522
|
+
def ruby_tslp_unsupported_top_level_lines(source, process_analysis)
|
|
1523
|
+
lines = normalize_source(source).split("\n", -1)
|
|
1524
|
+
claimed = ruby_tslp_claimed_line_indexes(lines, process_analysis)
|
|
1525
|
+
lines.each_index.filter_map do |index|
|
|
1526
|
+
next if claimed.include?(index)
|
|
1527
|
+
|
|
1528
|
+
line = lines[index]
|
|
1529
|
+
next if line.strip.empty? || comment_line?(line)
|
|
1530
|
+
|
|
1531
|
+
index + 1
|
|
1532
|
+
end
|
|
1533
|
+
end
|
|
1534
|
+
|
|
1535
|
+
def ruby_tslp_claimed_line_indexes(lines, process_analysis)
|
|
1536
|
+
claimed = Set.new
|
|
1537
|
+
ruby_top_level_process_structure_items(process_analysis).each do |item|
|
|
1538
|
+
start_index = attached_comment_start_index(lines, item.span.start_row)
|
|
1539
|
+
(start_index..item.span.end_row).each { |line_index| claimed.add(line_index) }
|
|
1540
|
+
end
|
|
1541
|
+
Array(process_analysis&.imports).each do |item|
|
|
1542
|
+
(item.span.start_row..item.span.end_row).each { |line_index| claimed.add(line_index) }
|
|
1543
|
+
end
|
|
1544
|
+
claimed
|
|
1545
|
+
end
|
|
1546
|
+
|
|
1547
|
+
def ruby_process_import_entries(source, process_analysis)
|
|
1548
|
+
lines = normalize_source(source).split("\n")
|
|
1549
|
+
Array(process_analysis&.imports).map do |item|
|
|
1550
|
+
text = lines[item.span.start_row..item.span.end_row].to_a.join("\n").rstrip
|
|
1551
|
+
{
|
|
1552
|
+
path: "/requires/#{item.source}",
|
|
1553
|
+
text: text,
|
|
1554
|
+
start_index: item.span.start_row,
|
|
1555
|
+
end_index: item.span.end_row
|
|
1556
|
+
}
|
|
1557
|
+
end
|
|
1558
|
+
end
|
|
1559
|
+
|
|
1560
|
+
def ruby_analysis_require_owners(source, process_analysis)
|
|
1561
|
+
imports = Array(process_analysis&.imports)
|
|
1562
|
+
unless imports.empty?
|
|
1563
|
+
return imports.each_with_index.map do |item, index|
|
|
1564
|
+
{
|
|
1565
|
+
path: "/requires/#{index}",
|
|
1566
|
+
owner_kind: 'require',
|
|
1567
|
+
match_key: item.source.to_s
|
|
1568
|
+
}
|
|
1569
|
+
end
|
|
1570
|
+
end
|
|
1571
|
+
|
|
1572
|
+
return [] if process_analysis
|
|
1573
|
+
|
|
1574
|
+
legacy_ruby_require_owners(source)
|
|
1575
|
+
end
|
|
1576
|
+
|
|
1577
|
+
def legacy_ruby_require_owners(source)
|
|
1578
|
+
requires = []
|
|
1579
|
+
normalize_source(source).split("\n").each do |line|
|
|
1580
|
+
match = REQUIRE_PATTERN.match(line)
|
|
1581
|
+
next unless match
|
|
1582
|
+
|
|
1583
|
+
requires << {
|
|
1584
|
+
path: "/requires/#{requires.length}",
|
|
1585
|
+
owner_kind: 'require',
|
|
1586
|
+
match_key: match[1]
|
|
1587
|
+
}
|
|
1588
|
+
end
|
|
1589
|
+
requires
|
|
1590
|
+
end
|
|
1591
|
+
|
|
1592
|
+
def ruby_process_import_item_at_line(process_analysis, line_number)
|
|
1593
|
+
index = line_number.to_i - 1
|
|
1594
|
+
Array(process_analysis&.imports).find do |item|
|
|
1595
|
+
item.span.start_row <= index && item.span.end_row >= index
|
|
1596
|
+
end
|
|
1597
|
+
end
|
|
1598
|
+
|
|
1599
|
+
def legacy_require_line?(line, process_analysis)
|
|
1600
|
+
return false if process_analysis
|
|
1601
|
+
|
|
1602
|
+
REQUIRE_PATTERN.match?(line)
|
|
1603
|
+
end
|
|
1604
|
+
|
|
1605
|
+
def ruby_tslp_file_footer_text(source, process_analysis)
|
|
1606
|
+
lines = normalize_source(source).split("\n")
|
|
1607
|
+
claimed = ruby_tslp_claimed_line_indexes(lines, process_analysis)
|
|
1608
|
+
footer_indexes = []
|
|
1609
|
+
(lines.length - 1).downto(0) do |index|
|
|
1610
|
+
break if claimed.include?(index)
|
|
1611
|
+
break unless lines[index].strip.empty? || comment_line?(lines[index])
|
|
1612
|
+
|
|
1613
|
+
footer_indexes.unshift(index)
|
|
1614
|
+
end
|
|
1615
|
+
lines.values_at(*footer_indexes).join("\n").strip
|
|
1616
|
+
end
|
|
1617
|
+
|
|
1618
|
+
def ruby_tslp_file_preamble_text(source, process_analysis)
|
|
1619
|
+
lines = normalize_source(source).split("\n")
|
|
1620
|
+
claimed = ruby_tslp_claimed_line_indexes(lines, process_analysis)
|
|
1621
|
+
preamble_indexes = []
|
|
1622
|
+
lines.each_index do |index|
|
|
1623
|
+
break if claimed.include?(index)
|
|
1624
|
+
break unless lines[index].strip.empty? || comment_line?(lines[index])
|
|
1625
|
+
|
|
1626
|
+
preamble_indexes << index
|
|
1627
|
+
end
|
|
1628
|
+
lines.values_at(*preamble_indexes).join("\n").strip
|
|
1629
|
+
end
|
|
1630
|
+
|
|
1631
|
+
def collect_ruby_declaration_entries(source, process_analysis: nil)
|
|
1632
|
+
process_entries = ruby_process_declaration_entries(source, process_analysis: process_analysis)
|
|
1633
|
+
return process_entries unless process_entries.empty?
|
|
1634
|
+
|
|
1635
|
+
legacy_collect_ruby_declaration_entries(source)
|
|
1636
|
+
end
|
|
1637
|
+
|
|
1638
|
+
def legacy_collect_ruby_declaration_entries(source)
|
|
1639
|
+
# TSLP process records are the preferred substrate. This legacy scanner is
|
|
1640
|
+
# retained only for direct helper calls that do not have parser analysis.
|
|
1641
|
+
# Main merge paths must pass process_analysis and fail closed when TSLP
|
|
1642
|
+
# cannot provide readable structure records.
|
|
1643
|
+
lines = normalize_source(source).split("\n")
|
|
1644
|
+
entries = []
|
|
1645
|
+
pending_comments = []
|
|
1646
|
+
index = 0
|
|
1647
|
+
|
|
1648
|
+
while index < lines.length
|
|
1649
|
+
line = lines[index]
|
|
1650
|
+
stripped = line.strip
|
|
1651
|
+
|
|
1652
|
+
if comment_line?(line)
|
|
1653
|
+
pending_comments << index
|
|
404
1654
|
index += 1
|
|
405
1655
|
next
|
|
406
1656
|
end
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
1657
|
+
|
|
1658
|
+
if stripped.empty?
|
|
1659
|
+
pending_comments = []
|
|
1660
|
+
index += 1
|
|
1661
|
+
next
|
|
1662
|
+
end
|
|
1663
|
+
|
|
1664
|
+
if REQUIRE_PATTERN.match?(line)
|
|
1665
|
+
pending_comments = []
|
|
1666
|
+
index += 1
|
|
1667
|
+
next
|
|
1668
|
+
end
|
|
1669
|
+
|
|
1670
|
+
declaration = declaration_for_line(line)
|
|
1671
|
+
unless declaration
|
|
1672
|
+
pending_comments = []
|
|
1673
|
+
index += 1
|
|
1674
|
+
next
|
|
1675
|
+
end
|
|
1676
|
+
|
|
1677
|
+
start_index = pending_comments.first || index
|
|
1678
|
+
depth = 1
|
|
1679
|
+
cursor = index + 1
|
|
1680
|
+
while cursor < lines.length
|
|
1681
|
+
candidate = lines[cursor].strip
|
|
1682
|
+
depth += 1 if declaration_for_line(candidate)
|
|
1683
|
+
if candidate == 'end'
|
|
1684
|
+
depth -= 1
|
|
1685
|
+
if depth.zero?
|
|
1686
|
+
cursor += 1
|
|
1687
|
+
break
|
|
1688
|
+
end
|
|
1689
|
+
end
|
|
1690
|
+
cursor += 1
|
|
1691
|
+
end
|
|
1692
|
+
|
|
1693
|
+
entries << {
|
|
1694
|
+
path: "/declarations/#{declaration[:name]}",
|
|
1695
|
+
name: declaration[:name],
|
|
1696
|
+
kind: declaration[:kind],
|
|
1697
|
+
merge_key: "#{declaration[:kind]}:#{declaration[:name]}",
|
|
1698
|
+
text: lines[start_index...cursor].join("\n").strip
|
|
1699
|
+
}
|
|
1700
|
+
pending_comments = []
|
|
1701
|
+
index = cursor
|
|
1702
|
+
end
|
|
1703
|
+
|
|
1704
|
+
entries
|
|
1705
|
+
end
|
|
1706
|
+
|
|
1707
|
+
def legacy_ruby_analysis_owner_entries(source)
|
|
1708
|
+
normalize_source(source).split("\n").filter_map do |line|
|
|
1709
|
+
declaration = declaration_for_line(line)
|
|
1710
|
+
next unless declaration
|
|
1711
|
+
|
|
1712
|
+
{
|
|
1713
|
+
path: "/declarations/#{declaration[:name]}",
|
|
1714
|
+
name: declaration[:name],
|
|
1715
|
+
kind: declaration[:kind],
|
|
1716
|
+
merge_key: "#{declaration[:kind]}:#{declaration[:name]}"
|
|
1717
|
+
}
|
|
1718
|
+
end
|
|
1719
|
+
end
|
|
1720
|
+
|
|
1721
|
+
def ruby_process_owner_entries(process_analysis)
|
|
1722
|
+
Array(process_analysis&.structure).filter_map do |item|
|
|
1723
|
+
kind = ruby_process_owner_kind(item)
|
|
1724
|
+
name = item.name.to_s
|
|
1725
|
+
next if kind.to_s.empty? || name.empty?
|
|
1726
|
+
|
|
1727
|
+
{
|
|
1728
|
+
path: "/declarations/#{name}",
|
|
1729
|
+
name: name,
|
|
1730
|
+
kind: kind,
|
|
1731
|
+
merge_key: "#{kind}:#{name}"
|
|
1732
|
+
}
|
|
1733
|
+
end
|
|
1734
|
+
end
|
|
1735
|
+
|
|
1736
|
+
def ruby_process_declaration_entries(source, process_analysis: nil)
|
|
1737
|
+
items = ruby_top_level_process_structure_items(process_analysis)
|
|
1738
|
+
return [] if items.empty?
|
|
1739
|
+
|
|
1740
|
+
lines = normalize_source(source).split("\n")
|
|
1741
|
+
items.map do |item|
|
|
1742
|
+
start_index = attached_comment_start_index(lines, item.span.start_row)
|
|
1743
|
+
finish_index = item.span.end_row
|
|
1744
|
+
kind = ruby_process_structure_kind(item)
|
|
1745
|
+
name = item.name.to_s
|
|
1746
|
+
{
|
|
1747
|
+
path: "/declarations/#{name}",
|
|
1748
|
+
name: name,
|
|
1749
|
+
kind: kind,
|
|
1750
|
+
merge_key: "#{kind}:#{name}",
|
|
1751
|
+
text: lines[start_index..finish_index].to_a.join("\n").strip,
|
|
1752
|
+
start_index: start_index,
|
|
1753
|
+
end_index: finish_index
|
|
1754
|
+
}
|
|
1755
|
+
end
|
|
1756
|
+
end
|
|
1757
|
+
|
|
1758
|
+
def ruby_top_level_section(text, entries)
|
|
1759
|
+
positioned_entries = entries.select do |entry|
|
|
1760
|
+
entry[:start_index].is_a?(Integer) && entry[:end_index].is_a?(Integer)
|
|
1761
|
+
end
|
|
1762
|
+
return { text: text } if positioned_entries.empty?
|
|
1763
|
+
|
|
1764
|
+
{
|
|
1765
|
+
text: text,
|
|
1766
|
+
start_index: positioned_entries.map { |entry| entry[:start_index] }.min,
|
|
1767
|
+
end_index: positioned_entries.map { |entry| entry[:end_index] }.max
|
|
1768
|
+
}
|
|
1769
|
+
end
|
|
1770
|
+
|
|
1771
|
+
def emit_ruby_top_level_sections(destination_source, sections)
|
|
1772
|
+
lines = normalize_source(destination_source).split("\n", -1)
|
|
1773
|
+
emitted = sections.reject { |section| section[:text].to_s.strip.empty? }
|
|
1774
|
+
previous = nil
|
|
1775
|
+
output = +''
|
|
1776
|
+
|
|
1777
|
+
emitted.each do |section|
|
|
1778
|
+
output << ruby_top_level_section_separator(lines, previous, section) if previous
|
|
1779
|
+
output << section.fetch(:text).strip
|
|
1780
|
+
previous = section
|
|
1781
|
+
end
|
|
1782
|
+
|
|
1783
|
+
"#{output.strip}\n"
|
|
1784
|
+
end
|
|
1785
|
+
|
|
1786
|
+
def ruby_top_level_section_separator(lines, previous, current)
|
|
1787
|
+
return "\n\n" unless previous[:end_index].is_a?(Integer) && current[:start_index].is_a?(Integer)
|
|
1788
|
+
return "\n\n" unless current[:start_index] > previous[:end_index]
|
|
1789
|
+
|
|
1790
|
+
gap = lines[(previous[:end_index] + 1)...current[:start_index]].to_a
|
|
1791
|
+
return "\n\n" if gap.empty?
|
|
1792
|
+
|
|
1793
|
+
"\n#{gap.join("\n")}\n"
|
|
1794
|
+
end
|
|
1795
|
+
|
|
1796
|
+
def ruby_top_level_process_structure_items(process_analysis)
|
|
1797
|
+
items = Array(process_analysis&.structure).select do |item|
|
|
1798
|
+
ruby_process_structure_kind(item) && !item.name.to_s.empty?
|
|
1799
|
+
end
|
|
1800
|
+
items.reject do |item|
|
|
1801
|
+
items.any? do |candidate|
|
|
1802
|
+
next false if candidate.equal?(item)
|
|
1803
|
+
|
|
1804
|
+
candidate.span.start_row <= item.span.start_row &&
|
|
1805
|
+
candidate.span.end_row >= item.span.end_row &&
|
|
1806
|
+
(candidate.span.start_row < item.span.start_row || candidate.span.end_row > item.span.end_row)
|
|
1807
|
+
end
|
|
1808
|
+
end.sort_by { |item| [item.span.start_row, item.span.start_col] }
|
|
1809
|
+
end
|
|
1810
|
+
|
|
1811
|
+
def ruby_process_structure_item_at_line(process_analysis, line_number)
|
|
1812
|
+
Array(process_analysis&.structure).find do |item|
|
|
1813
|
+
ruby_process_owner_kind(item) &&
|
|
1814
|
+
item.span.start_row == line_number - 1
|
|
1815
|
+
end&.then do |item|
|
|
1816
|
+
{ kind: ruby_process_owner_kind(item), name: item.name.to_s }
|
|
1817
|
+
end
|
|
1818
|
+
end
|
|
1819
|
+
|
|
1820
|
+
def ruby_process_owner_kind(item)
|
|
1821
|
+
case item.kind.to_s
|
|
1822
|
+
when 'class'
|
|
1823
|
+
'class'
|
|
1824
|
+
when 'module'
|
|
1825
|
+
'module'
|
|
1826
|
+
when 'method', 'function'
|
|
1827
|
+
'def'
|
|
1828
|
+
end
|
|
1829
|
+
end
|
|
1830
|
+
|
|
1831
|
+
def ruby_process_structure_kind(item)
|
|
1832
|
+
case item.kind.to_s
|
|
1833
|
+
when 'class'
|
|
1834
|
+
'class'
|
|
1835
|
+
when 'module'
|
|
1836
|
+
'module'
|
|
1837
|
+
when 'method', 'function'
|
|
1838
|
+
'def'
|
|
1839
|
+
end
|
|
1840
|
+
end
|
|
1841
|
+
|
|
1842
|
+
def attached_comment_start_index(lines, declaration_index)
|
|
1843
|
+
index = declaration_index.to_i
|
|
1844
|
+
index -= 1 while index.positive? && comment_line?(lines[index - 1])
|
|
1845
|
+
index
|
|
1846
|
+
end
|
|
1847
|
+
|
|
1848
|
+
def merge_ruby_declaration_entry(template_entry, destination_entry)
|
|
1849
|
+
return destination_entry unless template_entry
|
|
1850
|
+
|
|
1851
|
+
merged_text = merge_declaration_hash_constants(template_entry[:text], destination_entry[:text])
|
|
1852
|
+
merged_text = merge_declaration_body_constants(template_entry[:text], merged_text)
|
|
1853
|
+
merged_text = merge_declaration_body_methods(template_entry[:text], merged_text)
|
|
1854
|
+
merged_text = merge_nested_body_declarations(template_entry[:text], merged_text)
|
|
1855
|
+
destination_entry.merge(
|
|
1856
|
+
text: merged_text
|
|
1857
|
+
)
|
|
1858
|
+
end
|
|
1859
|
+
|
|
1860
|
+
def ruby_intra_owner_merge_plan(template_entries, destination_entries)
|
|
1861
|
+
template_by_key = template_entries.to_h { |entry| [entry[:merge_key], entry] }
|
|
1862
|
+
destination_entries.flat_map do |destination_entry|
|
|
1863
|
+
template_entry = template_by_key[destination_entry[:merge_key]]
|
|
1864
|
+
next [] unless template_entry
|
|
1865
|
+
next [] unless %w[class module].include?(destination_entry[:kind])
|
|
1866
|
+
|
|
1867
|
+
template_methods = direct_body_method_entries(template_entry[:text]).to_h { |entry| [entry[:signature], entry] }
|
|
1868
|
+
direct_body_method_entries(destination_entry[:text]).filter_map do |destination_method|
|
|
1869
|
+
template_method = template_methods[destination_method[:signature]]
|
|
1870
|
+
next unless template_method
|
|
1871
|
+
next if template_method[:body_text] == destination_method[:body_text]
|
|
1872
|
+
|
|
1873
|
+
{
|
|
1874
|
+
owner_path: destination_entry[:path],
|
|
1875
|
+
owner_kind: destination_entry[:kind],
|
|
1876
|
+
owner_name: destination_entry[:name],
|
|
1877
|
+
child_group: 'methods',
|
|
1878
|
+
child_signature: destination_method[:signature],
|
|
1879
|
+
child_path: "#{destination_entry[:path]}/methods/#{destination_method[:signature]}",
|
|
1880
|
+
decision: 'destination_wins',
|
|
1881
|
+
scope: 'owner_body'
|
|
1882
|
+
}
|
|
1883
|
+
end
|
|
1884
|
+
end
|
|
1885
|
+
end
|
|
1886
|
+
|
|
1887
|
+
def ruby_namespace_form_conflicts(template_entries, destination_entries)
|
|
1888
|
+
destination_names = destination_entries.to_h do |entry|
|
|
1889
|
+
["#{entry[:kind]}:#{entry[:name]}", true]
|
|
1890
|
+
end
|
|
1891
|
+
template_entries.flat_map do |entry|
|
|
1892
|
+
direct_body_declaration_entries(entry[:text]).filter_map do |nested_entry|
|
|
1893
|
+
compact_key = "#{nested_entry[:kind]}:#{entry[:name]}::#{nested_entry[:name]}"
|
|
1894
|
+
next unless destination_names[compact_key]
|
|
1895
|
+
|
|
1896
|
+
"#{entry[:name]}::#{nested_entry[:name]}"
|
|
1897
|
+
end
|
|
1898
|
+
end.uniq
|
|
1899
|
+
end
|
|
1900
|
+
|
|
1901
|
+
def source_owner_identity_entry(kind:, name:, parent_scope:, address:, content:)
|
|
1902
|
+
normalized_kind = kind.to_s
|
|
1903
|
+
normalized_name = name.to_s
|
|
1904
|
+
{
|
|
1905
|
+
owner_kind: normalized_kind,
|
|
1906
|
+
owner_name: normalized_name,
|
|
1907
|
+
parent_scope: parent_scope,
|
|
1908
|
+
address: address,
|
|
1909
|
+
structural_identity: "#{parent_scope}:#{normalized_kind}:#{normalized_name}",
|
|
1910
|
+
content_identity: "sha256:#{Digest::SHA256.hexdigest(content.to_s)}",
|
|
1911
|
+
identity_components: %w[owner_kind owner_name parent_scope content_identity]
|
|
1912
|
+
}
|
|
1913
|
+
end
|
|
1914
|
+
|
|
1915
|
+
def add_source_owner_occurrence_indexes(identities)
|
|
1916
|
+
counters = Hash.new(0)
|
|
1917
|
+
identities.map do |identity|
|
|
1918
|
+
occurrence_index = counters[identity[:structural_identity]]
|
|
1919
|
+
counters[identity[:structural_identity]] += 1
|
|
1920
|
+
identity.merge(
|
|
1921
|
+
occurrence_index: occurrence_index,
|
|
1922
|
+
address: occurrence_index.zero? ? identity[:address] : "#{identity[:address]}[#{occurrence_index}]"
|
|
1923
|
+
)
|
|
1924
|
+
end
|
|
1925
|
+
end
|
|
1926
|
+
|
|
1927
|
+
def ruby_method_identity_entries(source)
|
|
1928
|
+
collect_ruby_declaration_entries(source).flat_map do |declaration_entry|
|
|
1929
|
+
direct_body_method_entries(declaration_entry[:text]).map do |method_entry|
|
|
1930
|
+
{
|
|
1931
|
+
parent_scope: declaration_entry[:path],
|
|
1932
|
+
signature: method_entry[:signature],
|
|
1933
|
+
address: "#{declaration_entry[:path]}/methods/#{method_entry[:signature]}",
|
|
1934
|
+
normalized_body_identity: normalized_method_body_identity(method_entry[:body_text])
|
|
1935
|
+
}
|
|
1936
|
+
end
|
|
1937
|
+
end
|
|
1938
|
+
end
|
|
1939
|
+
|
|
1940
|
+
def normalized_method_body_identity(body_text)
|
|
1941
|
+
normalized_lines = body_text.to_s.lines.map.with_index do |line, index|
|
|
1942
|
+
index.zero? && DEF_PATTERN.match?(line) ? "#{line[/\A\s*/]}def __owner_name__\n" : line
|
|
1943
|
+
end
|
|
1944
|
+
"sha256:#{Digest::SHA256.hexdigest(normalized_lines.join)}"
|
|
1945
|
+
end
|
|
1946
|
+
|
|
1947
|
+
def ruby_method_shadowing(source)
|
|
1948
|
+
collect_ruby_declaration_entries(source).flat_map do |entry|
|
|
1949
|
+
direct_method_shadowing(entry)
|
|
1950
|
+
end
|
|
1951
|
+
end
|
|
1952
|
+
|
|
1953
|
+
def ruby_method_shadowing_diagnostics(source)
|
|
1954
|
+
ruby_method_shadowing(source).map do |entry|
|
|
1955
|
+
{
|
|
1956
|
+
severity: 'warning',
|
|
1957
|
+
category: 'ruby_method_shadowing',
|
|
1958
|
+
path: "#{entry[:owner_path]}/methods/#{entry[:method_signature]}",
|
|
1959
|
+
message: "Ruby method #{entry[:method_signature]} is defined #{entry[:shadowed_count] + 1} times in #{entry[:owner_path]}; the last definition shadows earlier definitions."
|
|
1960
|
+
}
|
|
1961
|
+
end
|
|
1962
|
+
end
|
|
1963
|
+
|
|
1964
|
+
def direct_method_shadowing(declaration_entry)
|
|
1965
|
+
grouped = direct_body_method_entries(declaration_entry[:text]).each_with_index.group_by do |(method_entry, _index)|
|
|
1966
|
+
method_entry[:signature]
|
|
1967
|
+
end
|
|
1968
|
+
|
|
1969
|
+
grouped.filter_map do |signature, entries|
|
|
1970
|
+
next if entries.length < 2
|
|
1971
|
+
|
|
1972
|
+
{
|
|
1973
|
+
owner_path: declaration_entry[:path],
|
|
1974
|
+
method_signature: signature,
|
|
1975
|
+
effective_index: entries.last[1],
|
|
1976
|
+
shadowed_indices: entries[0...-1].map { |_method_entry, index| index },
|
|
1977
|
+
shadowed_count: entries.length - 1
|
|
1978
|
+
}
|
|
1979
|
+
end
|
|
1980
|
+
end
|
|
1981
|
+
|
|
1982
|
+
def ruby_method_projection(source, revision:)
|
|
1983
|
+
collect_ruby_declaration_entries(source).flat_map do |declaration_entry|
|
|
1984
|
+
direct_body_method_entries(declaration_entry[:text]).each_with_index.map do |method_entry, index|
|
|
1985
|
+
signature = "method:#{declaration_entry[:path]}:#{method_entry[:signature]}"
|
|
1986
|
+
{
|
|
1987
|
+
path: "#{declaration_entry[:path]}/methods/#{index}",
|
|
1988
|
+
parent_path: "#{declaration_entry[:path]}/methods",
|
|
1989
|
+
node_id: "#{revision}:#{signature}",
|
|
1990
|
+
signature: signature,
|
|
1991
|
+
index: index
|
|
1992
|
+
}
|
|
1993
|
+
end
|
|
1994
|
+
end
|
|
1995
|
+
end
|
|
1996
|
+
|
|
1997
|
+
def qualified_nested_declaration_entries(entries)
|
|
1998
|
+
entries.flat_map do |entry|
|
|
1999
|
+
direct_body_declaration_entries(entry[:text]).map do |nested_entry|
|
|
2000
|
+
root_name = entry[:name]
|
|
2001
|
+
nested_name = nested_entry[:name]
|
|
2002
|
+
qualified_name = nested_name.include?('::') ? nested_name : "#{root_name}::#{nested_name}"
|
|
2003
|
+
nested_entry.merge(
|
|
2004
|
+
name: qualified_name,
|
|
2005
|
+
path: "/declarations/#{qualified_name}",
|
|
2006
|
+
merge_key: "#{nested_entry[:kind]}:#{qualified_name}",
|
|
2007
|
+
text: normalize_declaration_text_indent(nested_entry[:text]),
|
|
2008
|
+
namespace_root_merge_key: entry[:merge_key]
|
|
2009
|
+
)
|
|
2010
|
+
end
|
|
2011
|
+
end
|
|
2012
|
+
end
|
|
2013
|
+
|
|
2014
|
+
def normalize_declaration_text_indent(text)
|
|
2015
|
+
lines = text.to_s.split("\n")
|
|
2016
|
+
base_indent = lines.first.to_s[/\A\s*/].to_s
|
|
2017
|
+
return text if base_indent.empty?
|
|
2018
|
+
|
|
2019
|
+
lines.map do |line|
|
|
2020
|
+
line.start_with?(base_indent) ? line[base_indent.length..].to_s : line
|
|
2021
|
+
end.join("\n")
|
|
2022
|
+
end
|
|
2023
|
+
|
|
2024
|
+
def namespace_wrapper_matched?(entry, candidates, matched)
|
|
2025
|
+
children = candidates.select { |candidate| candidate[:namespace_root_merge_key] == entry[:merge_key] }
|
|
2026
|
+
return false if children.empty?
|
|
2027
|
+
unless direct_body_method_entries(entry[:text]).empty? && direct_body_constant_entries(entry[:text]).empty?
|
|
2028
|
+
return false
|
|
2029
|
+
end
|
|
2030
|
+
|
|
2031
|
+
children.all? { |child| matched[child[:merge_key]] }
|
|
2032
|
+
end
|
|
2033
|
+
|
|
2034
|
+
def merge_declaration_hash_constants(template_text, destination_text)
|
|
2035
|
+
template_blocks = constant_hash_blocks(template_text).to_h { |block| [block[:constant], block] }
|
|
2036
|
+
destination_blocks = constant_hash_blocks(destination_text)
|
|
2037
|
+
return destination_text if template_blocks.empty? || destination_blocks.empty?
|
|
2038
|
+
|
|
2039
|
+
output = destination_text.dup
|
|
2040
|
+
destination_blocks.reverse_each do |destination_block|
|
|
2041
|
+
template_block = template_blocks[destination_block[:constant]]
|
|
2042
|
+
next unless template_block
|
|
2043
|
+
|
|
2044
|
+
template_hash = RubyHashLiteralProjector.new(template_block[:hash_source]).call
|
|
2045
|
+
destination_hash = RubyHashLiteralProjector.new(destination_block[:hash_source]).call
|
|
2046
|
+
merged_hash = merge_ruby_hash_literals(template_hash, destination_hash)
|
|
2047
|
+
rendered = "#{destination_block[:prefix]}#{render_ruby_hash_literal(merged_hash,
|
|
2048
|
+
destination_block[:base_indent])}"
|
|
2049
|
+
output[destination_block[:range]] = rendered
|
|
2050
|
+
rescue ArgumentError
|
|
2051
|
+
next
|
|
2052
|
+
end
|
|
2053
|
+
output
|
|
2054
|
+
end
|
|
2055
|
+
|
|
2056
|
+
def merge_declaration_body_constants(template_text, destination_text)
|
|
2057
|
+
template_constants = direct_body_constant_entries(template_text)
|
|
2058
|
+
destination_constants = direct_body_constant_entries(destination_text)
|
|
2059
|
+
return destination_text if template_constants.empty?
|
|
2060
|
+
|
|
2061
|
+
merged_text = merge_matched_array_constants(template_constants, destination_constants, destination_text)
|
|
2062
|
+
destination_names = destination_constants.map { |entry| entry[:name] }.to_h { |name| [name, true] }
|
|
2063
|
+
missing_constants = template_constants.reject { |entry| destination_names[entry[:name]] }
|
|
2064
|
+
return merged_text if missing_constants.empty?
|
|
2065
|
+
|
|
2066
|
+
insert_declaration_body_blocks(merged_text, missing_constants.map do |entry|
|
|
2067
|
+
entry[:text]
|
|
2068
|
+
end, placement: :after_opening)
|
|
2069
|
+
end
|
|
2070
|
+
|
|
2071
|
+
def merge_matched_array_constants(template_constants, destination_constants, destination_text)
|
|
2072
|
+
template_by_name = template_constants.to_h { |entry| [entry[:name], entry] }
|
|
2073
|
+
output = destination_text.dup
|
|
2074
|
+
destination_constants.reverse_each do |destination_entry|
|
|
2075
|
+
template_entry = template_by_name[destination_entry[:name]]
|
|
2076
|
+
next unless template_entry
|
|
2077
|
+
|
|
2078
|
+
merged_text = merge_array_constant_text(template_entry[:text], destination_entry[:text])
|
|
2079
|
+
next unless merged_text
|
|
2080
|
+
|
|
2081
|
+
output[destination_entry[:range]] = merged_text
|
|
2082
|
+
end
|
|
2083
|
+
output
|
|
2084
|
+
end
|
|
2085
|
+
|
|
2086
|
+
def merge_array_constant_text(template_text, destination_text)
|
|
2087
|
+
template_match = template_text.match(/\A(\s*[A-Z]\w*\s*=\s*)\[(.*)\]\z/)
|
|
2088
|
+
destination_match = destination_text.match(/\A(\s*[A-Z]\w*\s*=\s*)\[(.*)\]\z/)
|
|
2089
|
+
unless template_match && destination_match
|
|
2090
|
+
return merge_percent_array_constant_text(template_text,
|
|
2091
|
+
destination_text) || merge_multiline_array_constant_text(template_text,
|
|
2092
|
+
destination_text)
|
|
2093
|
+
end
|
|
2094
|
+
|
|
2095
|
+
destination_elements = split_ruby_array_elements(destination_match[2])
|
|
2096
|
+
template_elements = split_ruby_array_elements(template_match[2])
|
|
2097
|
+
destination_keys = destination_elements.map do |element|
|
|
2098
|
+
normalize_array_element_key(element)
|
|
2099
|
+
end.to_h { |key| [key, true] }
|
|
2100
|
+
appended = template_elements.reject { |element| destination_keys[normalize_array_element_key(element)] }
|
|
2101
|
+
return destination_text if appended.empty?
|
|
2102
|
+
|
|
2103
|
+
"#{destination_match[1]}[#{(destination_elements + appended).join(', ')}]"
|
|
2104
|
+
end
|
|
2105
|
+
|
|
2106
|
+
def merge_percent_array_constant_text(template_text, destination_text)
|
|
2107
|
+
template_match = parse_percent_array_constant_text(template_text)
|
|
2108
|
+
destination_match = parse_percent_array_constant_text(destination_text)
|
|
2109
|
+
return unless template_match && destination_match
|
|
2110
|
+
|
|
2111
|
+
destination_elements = destination_match[:body].split(/\s+/).reject(&:empty?)
|
|
2112
|
+
template_elements = template_match[:body].split(/\s+/).reject(&:empty?)
|
|
2113
|
+
destination_keys = destination_elements.to_h { |element| [element, true] }
|
|
2114
|
+
appended = template_elements.reject { |element| destination_keys[element] }
|
|
2115
|
+
return destination_text if appended.empty?
|
|
2116
|
+
|
|
2117
|
+
"#{destination_match[:prefix]}#{(destination_elements + appended).join(' ')}#{destination_match[:closing]}"
|
|
2118
|
+
end
|
|
2119
|
+
|
|
2120
|
+
def parse_percent_array_constant_text(text)
|
|
2121
|
+
match = text.match(/\A(?<head>\s*[A-Z]\w*\s*=\s*%[wWiI])(?<opening>[^\s[:alnum:]])(?<content>.*)\z/)
|
|
2122
|
+
return unless match
|
|
2123
|
+
|
|
2124
|
+
closing = PERCENT_ARRAY_DELIMITER_PAIRS.fetch(match[:opening], match[:opening])
|
|
2125
|
+
content = match[:content]
|
|
2126
|
+
return unless content.end_with?(closing)
|
|
2127
|
+
|
|
2128
|
+
{
|
|
2129
|
+
prefix: "#{match[:head]}#{match[:opening]}",
|
|
2130
|
+
body: content[0...-closing.length],
|
|
2131
|
+
closing: closing
|
|
2132
|
+
}
|
|
2133
|
+
end
|
|
2134
|
+
|
|
2135
|
+
def merge_multiline_array_constant_text(template_text, destination_text)
|
|
2136
|
+
template_match = template_text.match(/\A(\s*[A-Z]\w*\s*=\s*\[\n)(.*)(\n\s*\])\z/m)
|
|
2137
|
+
destination_match = destination_text.match(/\A(\s*[A-Z]\w*\s*=\s*\[\n)(.*)(\n\s*\])\z/m)
|
|
2138
|
+
return unless template_match && destination_match
|
|
2139
|
+
|
|
2140
|
+
destination_elements = multiline_array_elements(destination_match[2])
|
|
2141
|
+
template_elements = multiline_array_elements(template_match[2])
|
|
2142
|
+
destination_keys = destination_elements.map do |element|
|
|
2143
|
+
normalize_array_element_key(element[:value])
|
|
2144
|
+
end.to_h { |key| [key, true] }
|
|
2145
|
+
appended = template_elements.reject { |element| destination_keys[normalize_array_element_key(element[:value])] }
|
|
2146
|
+
return destination_text if appended.empty?
|
|
2147
|
+
|
|
2148
|
+
insertion_prefix = destination_elements.last&.dig(:indent) || template_elements.first&.dig(:indent) || ' '
|
|
2149
|
+
body = append_multiline_array_elements(destination_match[2], appended, insertion_prefix)
|
|
2150
|
+
"#{destination_match[1]}#{body}#{destination_match[3]}"
|
|
2151
|
+
end
|
|
2152
|
+
|
|
2153
|
+
def merge_declaration_body_methods(template_text, destination_text)
|
|
2154
|
+
template_methods = direct_body_method_entries(template_text)
|
|
2155
|
+
destination_methods = direct_body_method_entries(destination_text)
|
|
2156
|
+
return destination_text if template_methods.empty?
|
|
2157
|
+
|
|
2158
|
+
destination_method_signatures = destination_methods.map do |entry|
|
|
2159
|
+
entry[:signature]
|
|
2160
|
+
end.to_h { |signature| [signature, true] }
|
|
2161
|
+
missing_methods = template_methods.reject { |entry| destination_method_signatures[entry[:signature]] }
|
|
2162
|
+
return destination_text if missing_methods.empty?
|
|
2163
|
+
|
|
2164
|
+
public_methods, visibility_methods = missing_methods.partition { |entry| entry[:visibility] == 'public' }
|
|
2165
|
+
merged_text = destination_text
|
|
2166
|
+
unless public_methods.empty?
|
|
2167
|
+
merged_text = insert_declaration_body_blocks(
|
|
2168
|
+
merged_text,
|
|
2169
|
+
public_methods.map { |entry| entry[:body_text] },
|
|
2170
|
+
before_visibility: !direct_visibility_section_present?(merged_text, 'public')
|
|
2171
|
+
)
|
|
2172
|
+
end
|
|
2173
|
+
visibility_methods.group_by { |entry| entry[:visibility] }.each do |visibility, entries|
|
|
2174
|
+
blocks = if direct_visibility_section_present?(merged_text, visibility)
|
|
2175
|
+
merged_text = insert_declaration_body_blocks(merged_text, entries.map do |entry|
|
|
2176
|
+
entry[:body_text]
|
|
2177
|
+
end, before_visibility: false)
|
|
2178
|
+
next
|
|
2179
|
+
else
|
|
2180
|
+
entries.map { |entry| entry[:text] }
|
|
2181
|
+
end
|
|
2182
|
+
merged_text = insert_declaration_body_blocks(merged_text, blocks)
|
|
2183
|
+
end
|
|
2184
|
+
merged_text
|
|
2185
|
+
end
|
|
2186
|
+
|
|
2187
|
+
def merge_nested_body_declarations(template_text, destination_text)
|
|
2188
|
+
template_entries = direct_body_declaration_entries(template_text)
|
|
2189
|
+
destination_entries = direct_body_declaration_entries(destination_text)
|
|
2190
|
+
return destination_text if template_entries.empty? || destination_entries.empty?
|
|
2191
|
+
|
|
2192
|
+
template_by_path = template_entries.to_h { |entry| [entry[:merge_key], entry] }
|
|
2193
|
+
output = destination_text.dup
|
|
2194
|
+
destination_entries.reverse_each do |destination_entry|
|
|
2195
|
+
template_entry = template_by_path[destination_entry[:merge_key]]
|
|
2196
|
+
next unless template_entry
|
|
2197
|
+
|
|
2198
|
+
output[destination_entry[:range]] = merge_ruby_declaration_entry(template_entry, destination_entry)[:text]
|
|
2199
|
+
end
|
|
2200
|
+
|
|
2201
|
+
destination_paths = destination_entries.map { |entry| entry[:merge_key] }.to_h { |path| [path, true] }
|
|
2202
|
+
missing_entries = template_entries.reject { |entry| destination_paths[entry[:merge_key]] }
|
|
2203
|
+
return output if missing_entries.empty?
|
|
2204
|
+
|
|
2205
|
+
insert_declaration_body_blocks(output, missing_entries.map { |entry| entry[:text] })
|
|
2206
|
+
end
|
|
2207
|
+
|
|
2208
|
+
def unsupported_feature_result(message)
|
|
2209
|
+
{
|
|
2210
|
+
ok: false,
|
|
2211
|
+
diagnostics: [{ severity: 'error', category: 'unsupported_feature', message: message }],
|
|
2212
|
+
policies: []
|
|
2213
|
+
}
|
|
2214
|
+
end
|
|
2215
|
+
|
|
2216
|
+
def normalize_method_move_policy(policy)
|
|
2217
|
+
normalized = policy.to_s.strip
|
|
2218
|
+
normalized = DEFAULT_METHOD_MOVE_POLICY if normalized.empty?
|
|
2219
|
+
return normalized if normalized == DEFAULT_METHOD_MOVE_POLICY
|
|
2220
|
+
|
|
2221
|
+
raise ArgumentError, "Unsupported Ruby method move policy #{policy.inspect}"
|
|
2222
|
+
end
|
|
2223
|
+
|
|
2224
|
+
private
|
|
2225
|
+
|
|
2226
|
+
def top_level_source_region_owners(lines)
|
|
2227
|
+
owners = []
|
|
2228
|
+
pending_comments = []
|
|
2229
|
+
index = 0
|
|
2230
|
+
|
|
2231
|
+
while index < lines.length
|
|
2232
|
+
line = lines[index]
|
|
2233
|
+
stripped = line.strip
|
|
2234
|
+
|
|
2235
|
+
if comment_line?(line)
|
|
2236
|
+
pending_comments << index
|
|
2237
|
+
index += 1
|
|
2238
|
+
next
|
|
2239
|
+
end
|
|
2240
|
+
|
|
2241
|
+
if stripped.empty?
|
|
2242
|
+
pending_comments = []
|
|
2243
|
+
index += 1
|
|
2244
|
+
next
|
|
2245
|
+
end
|
|
2246
|
+
|
|
2247
|
+
if (match = REQUIRE_PATTERN.match(line))
|
|
2248
|
+
require_path = match[1]
|
|
2249
|
+
owners << {
|
|
2250
|
+
region_id: "require:#{require_path}",
|
|
2251
|
+
region_kind: 'owner',
|
|
2252
|
+
owner_kind: 'require',
|
|
2253
|
+
address: "/requires/#{require_path}",
|
|
2254
|
+
match_key: require_path,
|
|
2255
|
+
start_index: index,
|
|
2256
|
+
end_index: index,
|
|
2257
|
+
span: source_report_line_span(index, index),
|
|
2258
|
+
content: source_report_region_content(lines, index, index)
|
|
2259
|
+
}
|
|
2260
|
+
pending_comments = []
|
|
2261
|
+
index += 1
|
|
2262
|
+
next
|
|
2263
|
+
end
|
|
2264
|
+
|
|
2265
|
+
declaration = declaration_for_line(line)
|
|
2266
|
+
if declaration
|
|
2267
|
+
start_index = pending_comments.first || index
|
|
2268
|
+
finish_index = ruby_block_finish_index(lines, index)
|
|
2269
|
+
address = declaration[:kind] == 'def' ? "/methods/#{declaration[:name]}" : "/declarations/#{declaration[:name]}"
|
|
2270
|
+
owner = {
|
|
2271
|
+
region_id: "#{declaration[:kind] == 'def' ? 'method' : 'declaration'}:#{declaration[:name]}",
|
|
2272
|
+
region_kind: 'owner',
|
|
2273
|
+
owner_kind: declaration[:kind],
|
|
2274
|
+
address: address,
|
|
2275
|
+
match_key: declaration[:name],
|
|
2276
|
+
start_index: start_index,
|
|
2277
|
+
declaration_start_index: index,
|
|
2278
|
+
end_index: finish_index,
|
|
2279
|
+
span: source_report_line_span(start_index, finish_index),
|
|
2280
|
+
declaration_span: source_report_line_span(index, finish_index),
|
|
2281
|
+
content: source_report_region_content(lines, start_index, finish_index)
|
|
2282
|
+
}
|
|
2283
|
+
owner[:child_regions] = container_child_source_regions(lines, declaration, index, finish_index) if %w[class
|
|
2284
|
+
module].include?(declaration[:kind])
|
|
2285
|
+
attached_comments = source_attached_comment_regions_for_report(
|
|
2286
|
+
lines: lines,
|
|
2287
|
+
start_index: start_index,
|
|
2288
|
+
declaration_index: index
|
|
2289
|
+
)
|
|
2290
|
+
owner[:attached_comments] = attached_comments unless attached_comments.empty?
|
|
2291
|
+
owners << owner
|
|
2292
|
+
pending_comments = []
|
|
2293
|
+
index = finish_index + 1
|
|
2294
|
+
next
|
|
2295
|
+
end
|
|
2296
|
+
|
|
2297
|
+
pending_comments = []
|
|
2298
|
+
index += 1
|
|
2299
|
+
end
|
|
2300
|
+
|
|
2301
|
+
owners
|
|
2302
|
+
end
|
|
2303
|
+
|
|
2304
|
+
def container_child_source_regions(lines, declaration, declaration_index, finish_index)
|
|
2305
|
+
owners = []
|
|
2306
|
+
pending_comments = []
|
|
2307
|
+
index = declaration_index + 1
|
|
2308
|
+
|
|
2309
|
+
while index < finish_index
|
|
2310
|
+
line = lines[index]
|
|
2311
|
+
stripped = line.strip
|
|
2312
|
+
|
|
2313
|
+
if comment_line?(line)
|
|
2314
|
+
pending_comments << index
|
|
2315
|
+
index += 1
|
|
2316
|
+
next
|
|
2317
|
+
end
|
|
2318
|
+
|
|
2319
|
+
if stripped.empty?
|
|
2320
|
+
pending_comments = []
|
|
2321
|
+
index += 1
|
|
2322
|
+
next
|
|
2323
|
+
end
|
|
2324
|
+
|
|
2325
|
+
nested_declaration = declaration_for_line(line)
|
|
2326
|
+
if nested_declaration && %w[class module].include?(nested_declaration[:kind])
|
|
2327
|
+
pending_comments = []
|
|
2328
|
+
index = ruby_block_finish_index(lines, index) + 1
|
|
2329
|
+
next
|
|
2330
|
+
end
|
|
2331
|
+
|
|
2332
|
+
method = DEF_PATTERN.match(line)
|
|
2333
|
+
unless method
|
|
2334
|
+
pending_comments = []
|
|
2335
|
+
index += 1
|
|
2336
|
+
next
|
|
2337
|
+
end
|
|
2338
|
+
|
|
2339
|
+
start_index = pending_comments.first || index
|
|
2340
|
+
method_finish_index = ruby_block_finish_index(lines, index)
|
|
2341
|
+
method_name = method[2]
|
|
2342
|
+
owner = {
|
|
2343
|
+
region_id: "method:#{declaration[:name]}##{method_name}",
|
|
2344
|
+
region_kind: 'owner',
|
|
2345
|
+
owner_kind: 'method',
|
|
2346
|
+
address: "/declarations/#{declaration[:name]}/methods/#{method_name}",
|
|
2347
|
+
match_key: method_name,
|
|
2348
|
+
start_index: start_index,
|
|
2349
|
+
end_index: method_finish_index,
|
|
2350
|
+
span: source_report_line_span(start_index, method_finish_index),
|
|
2351
|
+
content: source_report_region_content(lines, start_index, method_finish_index)
|
|
2352
|
+
}
|
|
2353
|
+
owner[:declaration_span] = source_report_line_span(index, method_finish_index) if start_index != index
|
|
2354
|
+
attached_comments = source_attached_comment_regions_for_report(
|
|
2355
|
+
lines: lines,
|
|
2356
|
+
start_index: start_index,
|
|
2357
|
+
declaration_index: index
|
|
2358
|
+
)
|
|
2359
|
+
owner[:attached_comments] = attached_comments unless attached_comments.empty?
|
|
2360
|
+
owners << owner
|
|
2361
|
+
pending_comments = []
|
|
2362
|
+
index = method_finish_index + 1
|
|
2363
|
+
end
|
|
2364
|
+
|
|
2365
|
+
source_interleaved_regions_for_report(
|
|
2366
|
+
lines: lines,
|
|
2367
|
+
owners: owners,
|
|
2368
|
+
container_name: declaration[:name],
|
|
2369
|
+
container_start_index: declaration_index,
|
|
2370
|
+
container_end_index: finish_index
|
|
2371
|
+
)
|
|
2372
|
+
end
|
|
2373
|
+
|
|
2374
|
+
def compact_region(region)
|
|
2375
|
+
region.reject { |_key, value| value.nil? }
|
|
2376
|
+
end
|
|
2377
|
+
|
|
2378
|
+
RubyHashNode = Struct.new(:pairs, :inline, :trailing_comma, keyword_init: true)
|
|
2379
|
+
RubyHashPair = Struct.new(:key, :key_source, :delimiter, :value, keyword_init: true)
|
|
2380
|
+
RubyScalarNode = Struct.new(:source, keyword_init: true)
|
|
2381
|
+
|
|
2382
|
+
class RubyHashLiteralProjector
|
|
2383
|
+
def initialize(source)
|
|
2384
|
+
@source = source.to_s
|
|
2385
|
+
end
|
|
2386
|
+
|
|
2387
|
+
def call
|
|
2388
|
+
tree = TreeHaver.parser_for(:ruby, backend_type: :tree_sitter).parse(source)
|
|
2389
|
+
root = if tree.respond_to?(:parse_result)
|
|
2390
|
+
tree.parse_result.value
|
|
2391
|
+
else
|
|
2392
|
+
tree.root_node
|
|
2393
|
+
end
|
|
2394
|
+
hash_node = root_hash_node(root)
|
|
2395
|
+
raise ArgumentError, 'expected Ruby hash literal' unless hash_node?(hash_node)
|
|
2396
|
+
|
|
2397
|
+
project_hash_node(hash_node)
|
|
2398
|
+
end
|
|
2399
|
+
|
|
2400
|
+
private
|
|
2401
|
+
|
|
2402
|
+
attr_reader :source
|
|
2403
|
+
|
|
2404
|
+
def root_hash_node(root)
|
|
2405
|
+
return root if hash_node?(root)
|
|
2406
|
+
return root.statements&.body&.first if root.respond_to?(:statements)
|
|
2407
|
+
|
|
2408
|
+
child_nodes(root).find { |child| hash_node?(child) }
|
|
2409
|
+
end
|
|
2410
|
+
|
|
2411
|
+
def hash_node?(node)
|
|
2412
|
+
%w[hash hash_node].include?(node&.type.to_s)
|
|
2413
|
+
end
|
|
2414
|
+
|
|
2415
|
+
def project_hash_node(node)
|
|
2416
|
+
pairs = hash_pair_nodes(node).map do |assoc|
|
|
2417
|
+
key_node, value_node = hash_pair_key_value_nodes(assoc)
|
|
2418
|
+
key = project_hash_key(key_node, hash_pair_operator(assoc))
|
|
2419
|
+
RubyHashPair.new(
|
|
2420
|
+
key: key.fetch(:key),
|
|
2421
|
+
key_source: key.fetch(:key_source),
|
|
2422
|
+
delimiter: key.fetch(:delimiter),
|
|
2423
|
+
value: project_hash_value(value_node)
|
|
2424
|
+
)
|
|
2425
|
+
end
|
|
2426
|
+
RubyHashNode.new(
|
|
2427
|
+
pairs: pairs,
|
|
2428
|
+
inline: !node_source(node).include?("\n"),
|
|
2429
|
+
trailing_comma: trailing_comma?(node)
|
|
2430
|
+
)
|
|
2431
|
+
end
|
|
2432
|
+
|
|
2433
|
+
def project_hash_value(node)
|
|
2434
|
+
return project_hash_node(node) if hash_node?(node)
|
|
2435
|
+
|
|
2436
|
+
RubyScalarNode.new(source: node_source(node).rstrip)
|
|
2437
|
+
end
|
|
2438
|
+
|
|
2439
|
+
def project_hash_key(key_node, operator)
|
|
2440
|
+
delimiter = operator.to_s == '=>' ? '=>' : ':'
|
|
2441
|
+
if delimiter == ':'
|
|
2442
|
+
{
|
|
2443
|
+
key: hash_key_value(key_node),
|
|
2444
|
+
key_source: node_source(key_node).delete_suffix(':'),
|
|
2445
|
+
delimiter: delimiter
|
|
2446
|
+
}
|
|
2447
|
+
elsif %w[symbol_node simple_symbol].include?(key_node.type.to_s) && node_source(key_node).start_with?(':')
|
|
2448
|
+
{
|
|
2449
|
+
key: hash_key_value(key_node),
|
|
2450
|
+
key_source: node_source(key_node),
|
|
2451
|
+
delimiter: delimiter
|
|
2452
|
+
}
|
|
2453
|
+
elsif %w[string_node string].include?(key_node.type.to_s)
|
|
2454
|
+
{
|
|
2455
|
+
key: hash_key_value(key_node),
|
|
2456
|
+
key_source: node_source(key_node),
|
|
2457
|
+
delimiter: delimiter
|
|
2458
|
+
}
|
|
2459
|
+
else
|
|
2460
|
+
{
|
|
2461
|
+
key: hash_key_value(key_node),
|
|
2462
|
+
key_source: node_source(key_node),
|
|
2463
|
+
delimiter: delimiter
|
|
2464
|
+
}
|
|
2465
|
+
end
|
|
2466
|
+
end
|
|
2467
|
+
|
|
2468
|
+
def hash_pair_nodes(node)
|
|
2469
|
+
return Array(node.elements) if node.respond_to?(:elements)
|
|
2470
|
+
|
|
2471
|
+
child_nodes(node).select { |child| child.type.to_s == 'pair' }
|
|
2472
|
+
end
|
|
2473
|
+
|
|
2474
|
+
def hash_pair_key_value_nodes(pair)
|
|
2475
|
+
return [pair.key, pair.value] if pair.respond_to?(:key) && pair.respond_to?(:value)
|
|
2476
|
+
|
|
2477
|
+
named = child_nodes(pair).reject { |child| punctuation_node?(child) }
|
|
2478
|
+
[named.first, named.last]
|
|
2479
|
+
end
|
|
2480
|
+
|
|
2481
|
+
def hash_pair_operator(pair)
|
|
2482
|
+
return pair.operator if pair.respond_to?(:operator)
|
|
2483
|
+
|
|
2484
|
+
all_child_nodes(pair).find { |child| %w[: =>].include?(child.type.to_s) }&.type
|
|
2485
|
+
end
|
|
2486
|
+
|
|
2487
|
+
def hash_key_value(node)
|
|
2488
|
+
return node.unescaped.to_s if node.respond_to?(:unescaped)
|
|
2489
|
+
|
|
2490
|
+
text = node_source(node)
|
|
2491
|
+
return text.delete_prefix(':') if text.start_with?(':')
|
|
2492
|
+
return text[1...-1] if node.type.to_s == 'string' && text.match?(/\A(["']).*\1\z/m)
|
|
2493
|
+
|
|
2494
|
+
text
|
|
2495
|
+
end
|
|
2496
|
+
|
|
2497
|
+
def trailing_comma?(node)
|
|
2498
|
+
if node.respond_to?(:elements) && node.respond_to?(:closing_loc)
|
|
2499
|
+
return trailing_comma_between?(last_child_end: Array(node.elements).last.location.end_offset,
|
|
2500
|
+
closing_start: node.closing_loc.start_offset)
|
|
411
2501
|
end
|
|
412
2502
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
2503
|
+
last_pair = hash_pair_nodes(node).last
|
|
2504
|
+
closing = all_child_nodes(node).reverse.find { |child| child.type.to_s == '}' }
|
|
2505
|
+
return false unless last_pair && closing
|
|
2506
|
+
|
|
2507
|
+
trailing_comma_between?(last_child_end: node_end_offset(last_pair), closing_start: node_start_offset(closing))
|
|
2508
|
+
end
|
|
2509
|
+
|
|
2510
|
+
def trailing_comma_between?(last_child_end:, closing_start:)
|
|
2511
|
+
source.byteslice(last_child_end...closing_start).to_s.include?(',')
|
|
2512
|
+
end
|
|
2513
|
+
|
|
2514
|
+
def child_nodes(node)
|
|
2515
|
+
if node.respond_to?(:compact_child_nodes)
|
|
2516
|
+
node.compact_child_nodes
|
|
2517
|
+
elsif node.respond_to?(:named_children)
|
|
2518
|
+
node.named_children
|
|
2519
|
+
elsif node.respond_to?(:children)
|
|
2520
|
+
node.children
|
|
2521
|
+
else
|
|
2522
|
+
[]
|
|
2523
|
+
end
|
|
2524
|
+
end
|
|
2525
|
+
|
|
2526
|
+
def all_child_nodes(node)
|
|
2527
|
+
if node.respond_to?(:compact_child_nodes)
|
|
2528
|
+
node.compact_child_nodes
|
|
2529
|
+
elsif node.respond_to?(:children)
|
|
2530
|
+
node.children
|
|
2531
|
+
else
|
|
2532
|
+
child_nodes(node)
|
|
422
2533
|
end
|
|
2534
|
+
end
|
|
2535
|
+
|
|
2536
|
+
def punctuation_node?(node)
|
|
2537
|
+
%w[{ } : => ,].include?(node.type.to_s)
|
|
2538
|
+
end
|
|
2539
|
+
|
|
2540
|
+
def node_source(node)
|
|
2541
|
+
return node.slice.to_s if node.respond_to?(:slice)
|
|
2542
|
+
return node.text.to_s if node.respond_to?(:text)
|
|
2543
|
+
|
|
2544
|
+
source.byteslice(node_start_offset(node)...node_end_offset(node)).to_s
|
|
2545
|
+
end
|
|
423
2546
|
|
|
424
|
-
|
|
2547
|
+
def node_start_offset(node)
|
|
2548
|
+
return node.location.start_offset if node.respond_to?(:location)
|
|
2549
|
+
return node.start_byte if node.respond_to?(:start_byte)
|
|
2550
|
+
|
|
2551
|
+
raise ArgumentError, 'Ruby hash node does not expose a start offset'
|
|
2552
|
+
end
|
|
2553
|
+
|
|
2554
|
+
def node_end_offset(node)
|
|
2555
|
+
return node.location.end_offset if node.respond_to?(:location)
|
|
2556
|
+
return node.end_byte if node.respond_to?(:end_byte)
|
|
2557
|
+
|
|
2558
|
+
raise ArgumentError, 'Ruby hash node does not expose an end offset'
|
|
2559
|
+
end
|
|
2560
|
+
end
|
|
2561
|
+
|
|
2562
|
+
def constant_hash_blocks(text)
|
|
2563
|
+
lines = text.to_s.split("\n", -1)
|
|
2564
|
+
line_start_offsets = []
|
|
2565
|
+
offset = 0
|
|
2566
|
+
lines.each do |line|
|
|
2567
|
+
line_start_offsets << offset
|
|
2568
|
+
offset += line.length + 1
|
|
2569
|
+
end
|
|
2570
|
+
|
|
2571
|
+
blocks = []
|
|
2572
|
+
index = 0
|
|
2573
|
+
while index < lines.length
|
|
2574
|
+
line = lines[index]
|
|
2575
|
+
match = CONSTANT_HASH_ASSIGNMENT_PATTERN.match(line)
|
|
425
2576
|
unless match
|
|
426
|
-
pending_comments = []
|
|
427
2577
|
index += 1
|
|
428
2578
|
next
|
|
429
2579
|
end
|
|
430
2580
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
2581
|
+
start_line = index
|
|
2582
|
+
finish_line = hash_assignment_finish_line(lines, start_line)
|
|
2583
|
+
if finish_line
|
|
2584
|
+
block_source = lines[start_line..finish_line].join("\n")
|
|
2585
|
+
hash_offset = block_source.index('{')
|
|
2586
|
+
start_offset = line_start_offsets[start_line]
|
|
2587
|
+
finish_offset = line_start_offsets[finish_line] + lines[finish_line].length
|
|
2588
|
+
blocks << {
|
|
2589
|
+
constant: match[2],
|
|
2590
|
+
prefix: block_source[0...hash_offset],
|
|
2591
|
+
hash_source: block_source[hash_offset..],
|
|
2592
|
+
base_indent: match[1].length,
|
|
2593
|
+
range: (start_offset...finish_offset)
|
|
2594
|
+
}
|
|
2595
|
+
index = finish_line + 1
|
|
2596
|
+
else
|
|
434
2597
|
index += 1
|
|
435
|
-
next
|
|
436
2598
|
end
|
|
437
|
-
|
|
438
|
-
start_index = pending_comments.first || index
|
|
439
|
-
finish_index = dsl_entry_finish_index(lines, index)
|
|
440
|
-
text = lines[start_index..finish_index].join("\n").strip
|
|
441
|
-
signature = dsl_entry_signature(name, line)
|
|
442
|
-
entries << { path: "/dsl/#{signature}", name: name, signature: signature, text: text } if signature
|
|
443
|
-
pending_comments = []
|
|
444
|
-
index = finish_index + 1
|
|
445
2599
|
end
|
|
446
|
-
|
|
447
|
-
entries
|
|
2600
|
+
blocks
|
|
448
2601
|
end
|
|
449
2602
|
|
|
450
|
-
def
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
2603
|
+
def hash_assignment_finish_line(lines, start_line)
|
|
2604
|
+
depth = 0
|
|
2605
|
+
in_string = nil
|
|
2606
|
+
escape = false
|
|
2607
|
+
start_line.upto(lines.length - 1) do |line_index|
|
|
2608
|
+
lines[line_index].each_char do |char|
|
|
2609
|
+
if in_string
|
|
2610
|
+
if escape
|
|
2611
|
+
escape = false
|
|
2612
|
+
elsif char == '\\'
|
|
2613
|
+
escape = true
|
|
2614
|
+
elsif char == in_string
|
|
2615
|
+
in_string = nil
|
|
2616
|
+
end
|
|
2617
|
+
next
|
|
2618
|
+
end
|
|
2619
|
+
|
|
2620
|
+
if ['"', "'"].include?(char)
|
|
2621
|
+
in_string = char
|
|
2622
|
+
elsif char == '{'
|
|
2623
|
+
depth += 1
|
|
2624
|
+
elsif char == '}'
|
|
2625
|
+
depth -= 1
|
|
2626
|
+
return line_index if depth.zero?
|
|
2627
|
+
end
|
|
460
2628
|
end
|
|
461
|
-
|
|
462
|
-
|
|
2629
|
+
end
|
|
2630
|
+
nil
|
|
463
2631
|
end
|
|
464
2632
|
|
|
465
|
-
def
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
end
|
|
2633
|
+
def direct_body_method_entries(text)
|
|
2634
|
+
lines = text.to_s.split("\n")
|
|
2635
|
+
return [] if lines.length < 3
|
|
469
2636
|
|
|
470
|
-
def collect_ruby_declaration_entries(source)
|
|
471
|
-
lines = normalize_source(source).split("\n")
|
|
472
2637
|
entries = []
|
|
473
2638
|
pending_comments = []
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
2639
|
+
current_visibility = 'public'
|
|
2640
|
+
visibility_start_index = nil
|
|
2641
|
+
visibility_consumed = false
|
|
2642
|
+
index = 1
|
|
2643
|
+
while index < lines.length - 1
|
|
477
2644
|
line = lines[index]
|
|
478
2645
|
stripped = line.strip
|
|
479
|
-
|
|
480
2646
|
if comment_line?(line)
|
|
481
2647
|
pending_comments << index
|
|
482
2648
|
index += 1
|
|
@@ -489,153 +2655,364 @@ module Ruby
|
|
|
489
2655
|
next
|
|
490
2656
|
end
|
|
491
2657
|
|
|
492
|
-
if
|
|
2658
|
+
if %w[private protected public].include?(stripped)
|
|
2659
|
+
current_visibility = stripped
|
|
2660
|
+
visibility_start_index = index
|
|
2661
|
+
visibility_consumed = false
|
|
493
2662
|
pending_comments = []
|
|
494
2663
|
index += 1
|
|
495
2664
|
next
|
|
496
2665
|
end
|
|
497
2666
|
|
|
498
|
-
|
|
499
|
-
|
|
2667
|
+
nested_declaration = declaration_for_line(line)
|
|
2668
|
+
if nested_declaration && %w[class module].include?(nested_declaration[:kind])
|
|
500
2669
|
pending_comments = []
|
|
501
|
-
index
|
|
2670
|
+
index = ruby_block_finish_index(lines, index) + 1
|
|
502
2671
|
next
|
|
503
2672
|
end
|
|
504
2673
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
depth += 1 if declaration_for_line(candidate)
|
|
511
|
-
if candidate == "end"
|
|
512
|
-
depth -= 1
|
|
513
|
-
if depth.zero?
|
|
514
|
-
cursor += 1
|
|
515
|
-
break
|
|
516
|
-
end
|
|
517
|
-
end
|
|
518
|
-
cursor += 1
|
|
2674
|
+
match = DEF_PATTERN.match(line)
|
|
2675
|
+
unless match
|
|
2676
|
+
pending_comments = []
|
|
2677
|
+
index += 1
|
|
2678
|
+
next
|
|
519
2679
|
end
|
|
520
2680
|
|
|
2681
|
+
start_index = pending_comments.first || visibility_section_start_index(visibility_start_index,
|
|
2682
|
+
visibility_consumed) || index
|
|
2683
|
+
finish_index = ruby_block_finish_index(lines, index)
|
|
521
2684
|
entries << {
|
|
522
|
-
|
|
523
|
-
|
|
2685
|
+
name: match[2],
|
|
2686
|
+
signature: SignatureSupport.textual_method_signature(match[1], match[2]),
|
|
2687
|
+
visibility: current_visibility,
|
|
2688
|
+
text: lines[start_index..finish_index].join("\n").rstrip,
|
|
2689
|
+
body_text: lines[(pending_comments.first || index)..finish_index].join("\n").rstrip
|
|
524
2690
|
}
|
|
525
2691
|
pending_comments = []
|
|
526
|
-
|
|
2692
|
+
visibility_consumed = true
|
|
2693
|
+
index = finish_index + 1
|
|
527
2694
|
end
|
|
528
|
-
|
|
529
2695
|
entries
|
|
530
2696
|
end
|
|
531
2697
|
|
|
532
|
-
def
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
2698
|
+
def direct_body_constant_entries(text)
|
|
2699
|
+
lines = text.to_s.split("\n")
|
|
2700
|
+
return [] if lines.length < 3
|
|
2701
|
+
|
|
2702
|
+
line_start_offsets = []
|
|
2703
|
+
offset = 0
|
|
2704
|
+
lines.each do |line|
|
|
2705
|
+
line_start_offsets << offset
|
|
2706
|
+
offset += line.length + 1
|
|
2707
|
+
end
|
|
2708
|
+
|
|
2709
|
+
entries = []
|
|
2710
|
+
index = 1
|
|
2711
|
+
while index < lines.length - 1
|
|
2712
|
+
stripped = lines[index].strip
|
|
2713
|
+
if stripped.empty? || comment_line?(lines[index])
|
|
2714
|
+
index += 1
|
|
2715
|
+
next
|
|
2716
|
+
end
|
|
2717
|
+
|
|
2718
|
+
nested_declaration = declaration_for_line(lines[index])
|
|
2719
|
+
if nested_declaration && %w[class module].include?(nested_declaration[:kind])
|
|
2720
|
+
index = ruby_block_finish_index(lines, index) + 1
|
|
2721
|
+
next
|
|
2722
|
+
end
|
|
2723
|
+
|
|
2724
|
+
match = CONSTANT_ASSIGNMENT_PATTERN.match(lines[index])
|
|
2725
|
+
unless match
|
|
2726
|
+
index += 1
|
|
2727
|
+
next
|
|
2728
|
+
end
|
|
2729
|
+
|
|
2730
|
+
finish_index = constant_assignment_finish_index(lines, index)
|
|
2731
|
+
entries << {
|
|
2732
|
+
name: match[2],
|
|
2733
|
+
text: lines[index..finish_index].join("\n").rstrip,
|
|
2734
|
+
range: (line_start_offsets[index]...(line_start_offsets[finish_index] + lines[finish_index].length))
|
|
2735
|
+
}
|
|
2736
|
+
index = finish_index + 1
|
|
2737
|
+
end
|
|
2738
|
+
entries
|
|
538
2739
|
end
|
|
539
2740
|
|
|
540
|
-
|
|
2741
|
+
def split_ruby_array_elements(source)
|
|
2742
|
+
elements = []
|
|
2743
|
+
start_index = 0
|
|
2744
|
+
string_quote = nil
|
|
2745
|
+
escape = false
|
|
2746
|
+
source.each_char.with_index do |char, index|
|
|
2747
|
+
if string_quote
|
|
2748
|
+
if escape
|
|
2749
|
+
escape = false
|
|
2750
|
+
elsif char == '\\'
|
|
2751
|
+
escape = true
|
|
2752
|
+
elsif char == string_quote
|
|
2753
|
+
string_quote = nil
|
|
2754
|
+
end
|
|
2755
|
+
next
|
|
2756
|
+
end
|
|
541
2757
|
|
|
542
|
-
|
|
543
|
-
|
|
2758
|
+
if ['"', "'"].include?(char)
|
|
2759
|
+
string_quote = char
|
|
2760
|
+
elsif char == ','
|
|
2761
|
+
elements << source[start_index...index].strip
|
|
2762
|
+
start_index = index + 1
|
|
2763
|
+
end
|
|
2764
|
+
end
|
|
2765
|
+
elements << source[start_index..].to_s.strip
|
|
2766
|
+
elements.reject(&:empty?)
|
|
544
2767
|
end
|
|
545
2768
|
|
|
546
|
-
def
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
2769
|
+
def normalize_array_element_key(element)
|
|
2770
|
+
element.to_s.strip
|
|
2771
|
+
end
|
|
2772
|
+
|
|
2773
|
+
def multiline_array_elements(source)
|
|
2774
|
+
source.to_s.lines.filter_map do |line|
|
|
2775
|
+
stripped = line.strip
|
|
2776
|
+
next if stripped.empty? || stripped.start_with?('#')
|
|
2777
|
+
|
|
2778
|
+
{
|
|
2779
|
+
indent: line[/\A\s*/],
|
|
2780
|
+
value: stripped.sub(/,\z/, '')
|
|
2781
|
+
}
|
|
553
2782
|
end
|
|
554
2783
|
end
|
|
555
2784
|
|
|
556
|
-
def
|
|
557
|
-
lines
|
|
558
|
-
|
|
2785
|
+
def append_multiline_array_elements(destination_body, appended, insertion_prefix)
|
|
2786
|
+
body_lines = destination_body.to_s.lines.map(&:chomp)
|
|
2787
|
+
element_indexes = body_lines.each_index.select do |index|
|
|
2788
|
+
stripped = body_lines[index].strip
|
|
2789
|
+
!stripped.empty? && !stripped.start_with?('#')
|
|
2790
|
+
end
|
|
2791
|
+
trailing_comma = element_indexes.empty? || body_lines[element_indexes.last].strip.end_with?(',')
|
|
559
2792
|
|
|
560
|
-
|
|
561
|
-
|
|
2793
|
+
if trailing_comma
|
|
2794
|
+
insertion_lines = appended.map { |element| "#{insertion_prefix}#{element[:value]}," }
|
|
2795
|
+
return "#{destination_body.rstrip}\n#{insertion_lines.join("\n")}"
|
|
562
2796
|
end
|
|
563
|
-
|
|
2797
|
+
|
|
2798
|
+
body_lines[element_indexes.last] = "#{body_lines[element_indexes.last]},"
|
|
2799
|
+
insertion_lines = appended.each_with_index.map do |element, index|
|
|
2800
|
+
suffix = index == appended.length - 1 ? '' : ','
|
|
2801
|
+
"#{insertion_prefix}#{element[:value]}#{suffix}"
|
|
2802
|
+
end
|
|
2803
|
+
"#{body_lines.join("\n").rstrip}\n#{insertion_lines.join("\n")}"
|
|
564
2804
|
end
|
|
565
2805
|
|
|
566
|
-
def
|
|
567
|
-
return
|
|
2806
|
+
def constant_assignment_finish_index(lines, index)
|
|
2807
|
+
return hash_assignment_finish_line(lines, index) || index if lines[index].include?('{')
|
|
2808
|
+
return array_assignment_finish_line(lines, index) || index if lines[index].include?('[')
|
|
568
2809
|
|
|
569
|
-
|
|
2810
|
+
index
|
|
570
2811
|
end
|
|
571
2812
|
|
|
572
|
-
def
|
|
2813
|
+
def array_assignment_finish_line(lines, start_line)
|
|
573
2814
|
depth = 0
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
2815
|
+
in_string = nil
|
|
2816
|
+
escape = false
|
|
2817
|
+
start_line.upto(lines.length - 1) do |line_index|
|
|
2818
|
+
lines[line_index].each_char do |char|
|
|
2819
|
+
if in_string
|
|
2820
|
+
if escape
|
|
2821
|
+
escape = false
|
|
2822
|
+
elsif char == '\\'
|
|
2823
|
+
escape = true
|
|
2824
|
+
elsif char == in_string
|
|
2825
|
+
in_string = nil
|
|
2826
|
+
end
|
|
2827
|
+
next
|
|
2828
|
+
end
|
|
581
2829
|
|
|
582
|
-
|
|
2830
|
+
if ['"', "'"].include?(char)
|
|
2831
|
+
in_string = char
|
|
2832
|
+
elsif char == '['
|
|
2833
|
+
depth += 1
|
|
2834
|
+
elsif char == ']'
|
|
2835
|
+
depth -= 1
|
|
2836
|
+
return line_index if depth.zero?
|
|
2837
|
+
end
|
|
2838
|
+
end
|
|
583
2839
|
end
|
|
584
|
-
|
|
2840
|
+
nil
|
|
2841
|
+
end
|
|
2842
|
+
|
|
2843
|
+
def visibility_section_start_index(index, consumed)
|
|
2844
|
+
return if consumed
|
|
2845
|
+
|
|
2846
|
+
index
|
|
2847
|
+
end
|
|
2848
|
+
|
|
2849
|
+
def direct_body_declaration_entries(text)
|
|
2850
|
+
lines = text.to_s.split("\n")
|
|
2851
|
+
return [] if lines.length < 3
|
|
2852
|
+
|
|
2853
|
+
line_start_offsets = []
|
|
2854
|
+
offset = 0
|
|
2855
|
+
lines.each do |line|
|
|
2856
|
+
line_start_offsets << offset
|
|
2857
|
+
offset += line.length + 1
|
|
2858
|
+
end
|
|
2859
|
+
|
|
2860
|
+
entries = []
|
|
2861
|
+
index = 1
|
|
2862
|
+
while index < lines.length - 1
|
|
2863
|
+
declaration = declaration_for_line(lines[index].strip)
|
|
2864
|
+
unless declaration && %w[class module].include?(declaration[:kind])
|
|
2865
|
+
index += 1
|
|
2866
|
+
next
|
|
2867
|
+
end
|
|
2868
|
+
|
|
2869
|
+
finish_index = ruby_block_finish_index(lines, index)
|
|
2870
|
+
start_offset = line_start_offsets[index]
|
|
2871
|
+
finish_offset = line_start_offsets[finish_index] + lines[finish_index].length
|
|
2872
|
+
entries << {
|
|
2873
|
+
path: "/declarations/#{declaration[:name]}",
|
|
2874
|
+
name: declaration[:name],
|
|
2875
|
+
kind: declaration[:kind],
|
|
2876
|
+
merge_key: "#{declaration[:kind]}:#{declaration[:name]}",
|
|
2877
|
+
text: lines[index..finish_index].join("\n").rstrip,
|
|
2878
|
+
range: (start_offset...finish_offset)
|
|
2879
|
+
}
|
|
2880
|
+
index = finish_index + 1
|
|
2881
|
+
end
|
|
2882
|
+
entries
|
|
2883
|
+
end
|
|
2884
|
+
|
|
2885
|
+
def declaration_closing_end_index(lines)
|
|
2886
|
+
depth = 0
|
|
2887
|
+
lines.each_with_index do |line, index|
|
|
2888
|
+
stripped = line.strip
|
|
2889
|
+
depth += 1 if declaration_for_line(stripped)
|
|
2890
|
+
depth -= 1 if stripped == 'end'
|
|
2891
|
+
return index if depth.zero? && index.positive?
|
|
2892
|
+
end
|
|
2893
|
+
nil
|
|
2894
|
+
end
|
|
2895
|
+
|
|
2896
|
+
def insert_declaration_body_blocks(destination_text, blocks, before_visibility: true, placement: :before_closing)
|
|
2897
|
+
lines = destination_text.to_s.split("\n")
|
|
2898
|
+
closing_index = declaration_closing_end_index(lines)
|
|
2899
|
+
return destination_text unless closing_index
|
|
2900
|
+
|
|
2901
|
+
insertion_index = if placement == :after_opening
|
|
2902
|
+
1
|
|
2903
|
+
elsif before_visibility
|
|
2904
|
+
direct_visibility_section_index(lines, closing_index) || closing_index
|
|
2905
|
+
else
|
|
2906
|
+
closing_index
|
|
2907
|
+
end
|
|
2908
|
+
insertion = []
|
|
2909
|
+
insertion << '' unless insertion_index == 1 || lines[insertion_index - 1].to_s.strip.empty?
|
|
2910
|
+
insertion.concat(blocks.join("\n\n").split("\n"))
|
|
2911
|
+
insertion << '' if insertion_index != closing_index && !lines[insertion_index].to_s.strip.empty?
|
|
2912
|
+
lines.insert(insertion_index, *insertion)
|
|
2913
|
+
"#{lines.join("\n").sub(/\n+\z/, '')}\n".chomp
|
|
585
2914
|
end
|
|
586
2915
|
|
|
587
|
-
def
|
|
588
|
-
|
|
589
|
-
|
|
2916
|
+
def direct_visibility_section_present?(text, visibility)
|
|
2917
|
+
lines = text.to_s.split("\n")
|
|
2918
|
+
closing_index = declaration_closing_end_index(lines)
|
|
2919
|
+
return false unless closing_index
|
|
590
2920
|
|
|
591
|
-
|
|
2921
|
+
find_direct_visibility_section_index(lines, closing_index, visibility: visibility)
|
|
592
2922
|
end
|
|
593
2923
|
|
|
594
|
-
def
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
2924
|
+
def direct_visibility_section_index(lines, closing_index)
|
|
2925
|
+
find_direct_visibility_section_index(lines, closing_index, visibility: nil)
|
|
2926
|
+
end
|
|
2927
|
+
|
|
2928
|
+
def find_direct_visibility_section_index(lines, closing_index, visibility:)
|
|
2929
|
+
depth = 1
|
|
2930
|
+
1.upto(closing_index - 1) do |index|
|
|
2931
|
+
stripped = lines[index].strip
|
|
2932
|
+
visibility_match = visibility ? stripped == visibility : %w[private protected].include?(stripped)
|
|
2933
|
+
return index if depth == 1 && visibility_match
|
|
2934
|
+
|
|
2935
|
+
depth += 1 if declaration_for_line(stripped)
|
|
2936
|
+
depth -= 1 if stripped == 'end'
|
|
2937
|
+
end
|
|
2938
|
+
nil
|
|
2939
|
+
end
|
|
2940
|
+
|
|
2941
|
+
def merge_ruby_hash_literals(template, destination)
|
|
2942
|
+
destination_by_key = destination.pairs.to_h { |pair| [pair.key, pair] }
|
|
2943
|
+
merged_pairs = template.pairs.map do |template_pair|
|
|
2944
|
+
destination_pair = destination_by_key[template_pair.key]
|
|
2945
|
+
if destination_pair.nil?
|
|
2946
|
+
template_pair
|
|
2947
|
+
elsif template_pair.value.is_a?(RubyHashNode) && destination_pair.value.is_a?(RubyHashNode)
|
|
2948
|
+
RubyHashPair.new(
|
|
2949
|
+
key: template_pair.key,
|
|
2950
|
+
key_source: destination_pair.key_source,
|
|
2951
|
+
delimiter: destination_pair.delimiter,
|
|
2952
|
+
value: merge_ruby_hash_literals(template_pair.value, destination_pair.value)
|
|
2953
|
+
)
|
|
2954
|
+
else
|
|
2955
|
+
destination_pair
|
|
2956
|
+
end
|
|
604
2957
|
end
|
|
2958
|
+
template_keys = template.pairs.map(&:key).to_h { |key| [key, true] }
|
|
2959
|
+
merged_pairs.concat(destination.pairs.reject { |pair| template_keys[pair.key] })
|
|
2960
|
+
RubyHashNode.new(pairs: merged_pairs, inline: destination.inline, trailing_comma: destination.trailing_comma)
|
|
605
2961
|
end
|
|
606
2962
|
|
|
607
|
-
def
|
|
608
|
-
return
|
|
2963
|
+
def render_ruby_hash_literal(node, base_indent)
|
|
2964
|
+
return node.source unless node.is_a?(RubyHashNode)
|
|
2965
|
+
return render_inline_ruby_hash_literal(node) if node.inline
|
|
609
2966
|
|
|
610
|
-
|
|
2967
|
+
child_indent = base_indent + 2
|
|
2968
|
+
lines = node.pairs.each_with_index.map do |pair, index|
|
|
2969
|
+
suffix = index == node.pairs.length - 1 && !node.trailing_comma ? '' : ','
|
|
2970
|
+
"#{' ' * child_indent}#{render_ruby_hash_key(pair)} #{render_ruby_hash_literal(pair.value,
|
|
2971
|
+
child_indent)}#{suffix}"
|
|
2972
|
+
end
|
|
2973
|
+
"{\n#{lines.join("\n")}\n#{' ' * base_indent}}"
|
|
611
2974
|
end
|
|
612
2975
|
|
|
613
|
-
def
|
|
614
|
-
|
|
2976
|
+
def render_inline_ruby_hash_literal(node)
|
|
2977
|
+
inner = node.pairs.map do |pair|
|
|
2978
|
+
"#{render_ruby_hash_key(pair)} #{render_ruby_hash_literal(pair.value, 0)}"
|
|
2979
|
+
end.join(', ')
|
|
2980
|
+
inner = "#{inner}," if node.trailing_comma && !inner.empty?
|
|
2981
|
+
"{#{inner}}"
|
|
615
2982
|
end
|
|
616
2983
|
|
|
617
|
-
def
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
2984
|
+
def render_ruby_hash_key(pair)
|
|
2985
|
+
delimiter = pair.delimiter == '=>' ? '=>' : ':'
|
|
2986
|
+
delimiter == '=>' ? "#{pair.key_source} =>" : "#{pair.key_source}:"
|
|
2987
|
+
end
|
|
621
2988
|
|
|
622
|
-
|
|
623
|
-
|
|
2989
|
+
def comment_line?(line)
|
|
2990
|
+
line.lstrip.start_with?('#')
|
|
2991
|
+
end
|
|
624
2992
|
|
|
625
|
-
|
|
626
|
-
|
|
2993
|
+
def declaration_for_line(line)
|
|
2994
|
+
if (match = CLASS_PATTERN.match(line))
|
|
2995
|
+
{ kind: 'class', name: match[1] }
|
|
2996
|
+
elsif (match = MODULE_PATTERN.match(line))
|
|
2997
|
+
{ kind: 'module', name: match[1] }
|
|
2998
|
+
elsif (match = DEF_PATTERN.match(line))
|
|
2999
|
+
{ kind: 'def', name: match[2], signature: SignatureSupport.textual_method_signature(match[1], match[2]) }
|
|
3000
|
+
end
|
|
3001
|
+
end
|
|
627
3002
|
|
|
628
|
-
|
|
629
|
-
|
|
3003
|
+
def ruby_block_finish_index(lines, start_index)
|
|
3004
|
+
depth = 0
|
|
3005
|
+
cursor = start_index
|
|
3006
|
+
while cursor < lines.length
|
|
3007
|
+
stripped = lines[cursor].strip
|
|
3008
|
+
depth += stripped.scan(/\bdo\b/).length
|
|
3009
|
+
depth += 1 if declaration_for_line(stripped) || stripped.match?(/\A(begin|if|unless|case|while|until|for)\b/)
|
|
3010
|
+
depth -= 1 if stripped == 'end'
|
|
3011
|
+
return cursor if depth <= 0 && cursor > start_index
|
|
630
3012
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
lines
|
|
634
|
-
insertion_index = lines.find_index { |line| line.strip == RAKEFILE_DEFAULT_TASK_DESC } + 1
|
|
635
|
-
insertion = task_block.dup
|
|
636
|
-
insertion << "" unless lines[insertion_index].to_s.strip.empty?
|
|
637
|
-
lines.insert(insertion_index, *insertion)
|
|
638
|
-
"#{lines.join("\n").sub(/\n+\z/, "")}\n"
|
|
3013
|
+
cursor += 1
|
|
3014
|
+
end
|
|
3015
|
+
lines.length - 1
|
|
639
3016
|
end
|
|
640
3017
|
|
|
641
3018
|
def preceding_code_line_index(lines, start_index)
|
|
@@ -663,14 +3040,14 @@ module Ruby
|
|
|
663
3040
|
start_line = filtered_entries.first[:line]
|
|
664
3041
|
end_line = filtered_entries.last[:line]
|
|
665
3042
|
doc_surface = Ast::Merge.discovered_surface(
|
|
666
|
-
surface_kind:
|
|
667
|
-
declared_language:
|
|
668
|
-
effective_language:
|
|
3043
|
+
surface_kind: 'ruby_doc_comment',
|
|
3044
|
+
declared_language: 'yard',
|
|
3045
|
+
effective_language: 'yard',
|
|
669
3046
|
address: "document[0] > ruby_doc_comment[#{owner_name}]",
|
|
670
|
-
parent_address:
|
|
671
|
-
owner: Ast::Merge.surface_owner_ref(kind:
|
|
3047
|
+
parent_address: 'document[0]',
|
|
3048
|
+
owner: Ast::Merge.surface_owner_ref(kind: 'owned_region', address: "/declarations/#{owner_name}"),
|
|
672
3049
|
span: Ast::Merge.surface_span(start_line: start_line, end_line: end_line),
|
|
673
|
-
reconstruction_strategy:
|
|
3050
|
+
reconstruction_strategy: 'rewrite_with_prefix_preservation',
|
|
674
3051
|
metadata: {
|
|
675
3052
|
owner_signature: owner_name,
|
|
676
3053
|
comment_prefix: comment_prefix_for(filtered_entries.first[:raw]),
|
|
@@ -683,33 +3060,22 @@ module Ruby
|
|
|
683
3060
|
|
|
684
3061
|
def example_surfaces_for(surface)
|
|
685
3062
|
entries = Array(surface.dig(:metadata, :entries))
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
match = EXAMPLE_TAG.match(content)
|
|
690
|
-
next unless match
|
|
691
|
-
|
|
692
|
-
body_start = tag_index + 1
|
|
693
|
-
body_end = next_tag_index(normalized, body_start) || normalized.length
|
|
694
|
-
next if body_start >= body_end
|
|
695
|
-
|
|
696
|
-
body_entries = entries[body_start...body_end]
|
|
697
|
-
next if body_entries.nil? || body_entries.empty?
|
|
698
|
-
|
|
699
|
-
declared_language = declared_example_language(match[:rest]) || "ruby"
|
|
3063
|
+
DocCommentSupport.example_blocks(entries).map do |block|
|
|
3064
|
+
body_entries = block.fetch(:body_entries)
|
|
3065
|
+
declared_language = block.fetch(:declared_language) || 'ruby'
|
|
700
3066
|
Ast::Merge.discovered_surface(
|
|
701
|
-
surface_kind:
|
|
3067
|
+
surface_kind: 'yard_example_block',
|
|
702
3068
|
declared_language: declared_language,
|
|
703
3069
|
effective_language: declared_language,
|
|
704
|
-
address: "#{surface[:address]} > yard_example[#{tag_index}]",
|
|
3070
|
+
address: "#{surface[:address]} > yard_example[#{block.fetch(:tag_index)}]",
|
|
705
3071
|
parent_address: surface[:address],
|
|
706
|
-
owner: Ast::Merge.surface_owner_ref(kind:
|
|
3072
|
+
owner: Ast::Merge.surface_owner_ref(kind: 'owned_region', address: surface[:address]),
|
|
707
3073
|
span: Ast::Merge.surface_span(start_line: body_entries.first[:line], end_line: body_entries.last[:line]),
|
|
708
|
-
reconstruction_strategy:
|
|
3074
|
+
reconstruction_strategy: 'rewrite_with_prefix_preservation',
|
|
709
3075
|
metadata: {
|
|
710
|
-
tag_kind:
|
|
711
|
-
tag_index: tag_index,
|
|
712
|
-
tag_text:
|
|
3076
|
+
tag_kind: 'example',
|
|
3077
|
+
tag_index: block.fetch(:tag_index),
|
|
3078
|
+
tag_text: block.fetch(:tag_text),
|
|
713
3079
|
comment_prefix: surface.dig(:metadata, :comment_prefix)
|
|
714
3080
|
}
|
|
715
3081
|
)
|
|
@@ -717,11 +3083,7 @@ module Ruby
|
|
|
717
3083
|
end
|
|
718
3084
|
|
|
719
3085
|
def next_tag_index(normalized_lines, start_index)
|
|
720
|
-
normalized_lines
|
|
721
|
-
next if index < start_index
|
|
722
|
-
return index if TAG_PREFIX.match?(content)
|
|
723
|
-
end
|
|
724
|
-
nil
|
|
3086
|
+
DocCommentSupport.next_tag_index(normalized_lines, start_index)
|
|
725
3087
|
end
|
|
726
3088
|
|
|
727
3089
|
def normalize_source(source)
|
|
@@ -729,37 +3091,30 @@ module Ruby
|
|
|
729
3091
|
end
|
|
730
3092
|
|
|
731
3093
|
def normalize_comment_content(raw)
|
|
732
|
-
|
|
3094
|
+
DocCommentSupport.normalize_comment_content(raw)
|
|
733
3095
|
end
|
|
734
3096
|
|
|
735
3097
|
def doc_comment_content?(raw)
|
|
736
|
-
|
|
737
|
-
return false if content.empty?
|
|
738
|
-
return false if DIRECTIVE_LINE.match?(content)
|
|
739
|
-
return false if MAGIC_COMMENT_PREFIXES.any? { |prefix| content.start_with?("#{prefix}:") }
|
|
740
|
-
|
|
741
|
-
true
|
|
3098
|
+
DocCommentSupport.doc_comment_content?(raw)
|
|
742
3099
|
end
|
|
743
3100
|
|
|
744
3101
|
def comment_prefix_for(raw)
|
|
745
|
-
raw
|
|
3102
|
+
DocCommentSupport.comment_prefix_for(raw)
|
|
746
3103
|
end
|
|
747
3104
|
|
|
748
3105
|
def declared_example_language(rest)
|
|
749
|
-
|
|
750
|
-
language = match && match[:language]
|
|
751
|
-
return if language.nil? || language.empty?
|
|
752
|
-
|
|
753
|
-
language.downcase.tr("-", "_")
|
|
3106
|
+
DocCommentSupport.declared_example_language(rest)
|
|
754
3107
|
end
|
|
755
3108
|
|
|
756
3109
|
module_function(
|
|
757
3110
|
:ruby_feature_profile,
|
|
758
3111
|
:available_ruby_backends,
|
|
3112
|
+
:ruby_tslp_capability_profile,
|
|
759
3113
|
:ruby_backend_feature_profile,
|
|
760
3114
|
:ruby_plan_context,
|
|
761
3115
|
:parse_ruby,
|
|
762
3116
|
:match_ruby_owners,
|
|
3117
|
+
:ruby_method_move_detection,
|
|
763
3118
|
:merge_ruby,
|
|
764
3119
|
:ruby_discovered_surfaces,
|
|
765
3120
|
:ruby_delegated_child_operations,
|
|
@@ -771,9 +3126,56 @@ module Ruby
|
|
|
771
3126
|
:merge_ruby_with_reviewed_nested_outputs_from_review_state_envelope,
|
|
772
3127
|
:merge_ruby_with_nested_outputs,
|
|
773
3128
|
:analyze_ruby_document,
|
|
774
|
-
:collect_ruby_require_entries,
|
|
775
3129
|
:collect_ruby_declaration_entries,
|
|
776
3130
|
:unsupported_feature_result
|
|
777
3131
|
)
|
|
778
3132
|
end
|
|
779
3133
|
end
|
|
3134
|
+
|
|
3135
|
+
Ruby::Merge.register_backend!
|
|
3136
|
+
require_relative 'merge/provider'
|
|
3137
|
+
Ruby::Merge.register_provider!
|
|
3138
|
+
|
|
3139
|
+
TreeHaver::BackendRegistry.register_tag(
|
|
3140
|
+
:tslp_ruby_import_records,
|
|
3141
|
+
category: :capability,
|
|
3142
|
+
backend_name: :tslp_ruby_import_records
|
|
3143
|
+
) do
|
|
3144
|
+
result = Ruby::Merge.parse_ruby("require \"json\"\n", 'ruby')
|
|
3145
|
+
result[:ok] && Array(result.dig(:analysis, :owners)).any? do |owner|
|
|
3146
|
+
owner[:owner_kind] == 'require' && owner[:match_key] == 'json'
|
|
3147
|
+
end
|
|
3148
|
+
end
|
|
3149
|
+
|
|
3150
|
+
TreeHaver::BackendRegistry.register_tag(
|
|
3151
|
+
:tslp_ruby_top_level_call_records,
|
|
3152
|
+
category: :capability,
|
|
3153
|
+
backend_name: :tslp_ruby_top_level_call_records
|
|
3154
|
+
) do
|
|
3155
|
+
template = <<~RUBY
|
|
3156
|
+
source "https://gem.coop"
|
|
3157
|
+
gemspec
|
|
3158
|
+
eval_gemfile "gemfiles/modular/style.gemfile"
|
|
3159
|
+
gem "rake"
|
|
3160
|
+
RUBY
|
|
3161
|
+
destination = <<~RUBY
|
|
3162
|
+
source "https://rubygems.org"
|
|
3163
|
+
gem "rspec"
|
|
3164
|
+
eval_gemfile "gemfiles/modular/style.gemfile"
|
|
3165
|
+
RUBY
|
|
3166
|
+
result = Ruby::Merge.merge_ruby(template, destination, 'ruby')
|
|
3167
|
+
result[:ok]
|
|
3168
|
+
end
|
|
3169
|
+
|
|
3170
|
+
TreeHaver::BackendRegistry.register_tag(
|
|
3171
|
+
:tslp_ruby_namespace_form_equivalence,
|
|
3172
|
+
category: :capability,
|
|
3173
|
+
backend_name: :tslp_ruby_namespace_form_equivalence
|
|
3174
|
+
) do
|
|
3175
|
+
template = Ruby::Merge.parse_ruby("module Admin\n class User\n end\nend\n", 'ruby')
|
|
3176
|
+
destination = Ruby::Merge.parse_ruby("class Admin::User\nend\n", 'ruby')
|
|
3177
|
+
template[:ok] && destination[:ok] && Ruby::Merge.ruby_namespace_form_conflicts(
|
|
3178
|
+
Ruby::Merge.send(:ruby_tslp_merge_context, template.fetch(:analysis), role: 'template').fetch(:declarations),
|
|
3179
|
+
Ruby::Merge.send(:ruby_tslp_merge_context, destination.fetch(:analysis), role: 'destination').fetch(:declarations)
|
|
3180
|
+
).empty?
|
|
3181
|
+
end
|