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