ruby-merge 7.1.3 → 7.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/README.md +1 -1
- data/lib/ruby/merge/block_binding_support.rb +22 -0
- data/lib/ruby/merge/block_directive_detector.rb +220 -0
- data/lib/ruby/merge/doc_comment_support.rb +101 -0
- data/lib/ruby/merge/gemspec_support.rb +33 -0
- data/lib/ruby/merge/magic_comment_support.rb +132 -0
- data/lib/ruby/merge/method_similarity.rb +81 -0
- data/lib/ruby/merge/nocov_node_base.rb +79 -0
- data/lib/ruby/merge/nocov_wrapper_base.rb +46 -0
- data/lib/ruby/merge/rescue_semantics.rb +219 -0
- data/lib/ruby/merge/scaffold_chunk_support.rb +75 -0
- data/lib/ruby/merge/signature_support.rb +98 -0
- data/lib/ruby/merge/version.rb +1 -1
- data/lib/ruby/merge.rb +113 -30
- data/sig/ruby/merge.rbs +0 -13
- data.tar.gz.sig +0 -0
- metadata +27 -31
- metadata.gz.sig +0 -0
- data/lib/ruby/merge/provider.rb +0 -77
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ruby
|
|
4
|
+
module Merge
|
|
5
|
+
# Shared Ruby wrapper for nodes carrying unbalanced or inline nocov markers.
|
|
6
|
+
#
|
|
7
|
+
# The wrapped parser node remains responsible for structural identity; this
|
|
8
|
+
# wrapper only marks it as a Ruby coverage directive participant.
|
|
9
|
+
class NocovWrapperBase
|
|
10
|
+
include Ast::Merge::BlockDirective
|
|
11
|
+
|
|
12
|
+
attr_reader :node, :merge_type
|
|
13
|
+
|
|
14
|
+
def initialize(node, merge_type = :nocov)
|
|
15
|
+
@node = node
|
|
16
|
+
@merge_type = merge_type
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def kind = :nocov
|
|
20
|
+
def children = []
|
|
21
|
+
def merge_policy = nil
|
|
22
|
+
|
|
23
|
+
def start_line
|
|
24
|
+
@node.location&.start_line
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def end_line
|
|
28
|
+
@node.location&.end_line
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def unwrap = @node
|
|
32
|
+
|
|
33
|
+
def location = @node.location
|
|
34
|
+
|
|
35
|
+
def slice = @node.slice
|
|
36
|
+
|
|
37
|
+
def nocov_wrapper? = true
|
|
38
|
+
def nocov_node? = false
|
|
39
|
+
def block_directive? = true
|
|
40
|
+
|
|
41
|
+
def inspect
|
|
42
|
+
"#<#{self.class} merge_type=#{@merge_type.inspect} node=#{@node.inspect}>"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ruby
|
|
4
|
+
module Merge
|
|
5
|
+
# Normalizes rescue clause ordering while preserving source-defined
|
|
6
|
+
# exception hierarchies and Ruby's broader-handler semantics.
|
|
7
|
+
# rubocop:disable Metrics/AbcSize, Metrics/ClassLength, Metrics/CyclomaticComplexity
|
|
8
|
+
# rubocop:disable Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
9
|
+
class RescueSemantics
|
|
10
|
+
def initialize(source_defined_exception_definitions: [])
|
|
11
|
+
@source_defined_exception_definitions = source_defined_exception_definitions
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def merge_ordered_clause_types(primary_types, secondary_types)
|
|
15
|
+
ordered = primary_types.dup
|
|
16
|
+
|
|
17
|
+
secondary_types.each_with_index do |clause_type, secondary_index|
|
|
18
|
+
next if ordered.include?(clause_type)
|
|
19
|
+
|
|
20
|
+
previous_shared = secondary_types[0...secondary_index].reverse.find { |type| ordered.include?(type) }
|
|
21
|
+
next_shared = secondary_types[(secondary_index + 1)..]&.find { |type| ordered.include?(type) }
|
|
22
|
+
|
|
23
|
+
if previous_shared
|
|
24
|
+
insert_at = ordered.index(previous_shared) + 1
|
|
25
|
+
ordered.insert(insert_at, clause_type)
|
|
26
|
+
elsif next_shared
|
|
27
|
+
insert_at = ordered.index(next_shared)
|
|
28
|
+
ordered.insert(insert_at, clause_type)
|
|
29
|
+
else
|
|
30
|
+
ordered << clause_type
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
ordered
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def canonicalize_rescue_clause_order(clause_types)
|
|
38
|
+
rescue_clause_types = clause_types.select { |clause_type| rescue_clause_type?(clause_type) }
|
|
39
|
+
return clause_types if rescue_clause_types.length < 2
|
|
40
|
+
|
|
41
|
+
ordered_rescue_types = rescue_clause_types.dup
|
|
42
|
+
|
|
43
|
+
if rescue_clause_types.any? { |clause_type| broad_rescue_clause_type?(clause_type) } &&
|
|
44
|
+
rescue_clause_types.any? { |clause_type| !broad_rescue_clause_type?(clause_type) }
|
|
45
|
+
specific_rescue_types = ordered_rescue_types.reject { |clause_type| broad_rescue_clause_type?(clause_type) }
|
|
46
|
+
broad_rescue_types = ordered_rescue_types.select { |clause_type| broad_rescue_clause_type?(clause_type) }
|
|
47
|
+
ordered_rescue_types = specific_rescue_types + broad_rescue_types
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
loop do
|
|
51
|
+
swapped = false
|
|
52
|
+
|
|
53
|
+
(0...(ordered_rescue_types.length - 1)).each do |index|
|
|
54
|
+
left_clause_type = ordered_rescue_types[index]
|
|
55
|
+
right_clause_type = ordered_rescue_types[index + 1]
|
|
56
|
+
next unless broader_rescue_clause_type_than?(left_clause_type, right_clause_type)
|
|
57
|
+
|
|
58
|
+
ordered_rescue_types[index] = right_clause_type
|
|
59
|
+
ordered_rescue_types[index + 1] = left_clause_type
|
|
60
|
+
swapped = true
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
break unless swapped
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
clause_types.map do |clause_type|
|
|
67
|
+
rescue_clause_type?(clause_type) ? ordered_rescue_types.shift : clause_type
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def canonicalize_begin_clause_kind_order(clause_types)
|
|
72
|
+
clause_types.each_with_index
|
|
73
|
+
.sort_by { |(clause_type, index)| [clause_kind_sort_key(clause_type), index] }
|
|
74
|
+
.map(&:first)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def rescue_clause_signature(exception_names)
|
|
78
|
+
normalized_exceptions = Array(exception_names).filter_map { |exception| normalize_exception_name(exception) }
|
|
79
|
+
if normalized_exceptions.empty? || normalized_exceptions == ['StandardError']
|
|
80
|
+
[:standard_error]
|
|
81
|
+
else
|
|
82
|
+
normalized_exceptions.sort
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def rescue_clause_type?(clause_type)
|
|
87
|
+
clause_type.is_a?(Array) && clause_type.first == :rescue_clause
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def broad_rescue_clause_type?(clause_type)
|
|
91
|
+
rescue_clause_type?(clause_type) && clause_type[1] == [:standard_error]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def clause_kind_sort_key(clause_type)
|
|
95
|
+
return 0 if rescue_clause_type?(clause_type)
|
|
96
|
+
return 1 if clause_type == :else_clause
|
|
97
|
+
return 2 if clause_type == :ensure_clause
|
|
98
|
+
|
|
99
|
+
3
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def normalize_exception_name(exception_name)
|
|
103
|
+
return 'StandardError' if exception_name == :standard_error
|
|
104
|
+
|
|
105
|
+
name = exception_name.to_s.sub(/\A::/, '')
|
|
106
|
+
name.empty? ? nil : name
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def qualify_source_constant_name(constant_name, namespace = nil)
|
|
110
|
+
normalized_name = normalize_exception_name(constant_name)
|
|
111
|
+
return if normalized_name.nil?
|
|
112
|
+
return normalized_name if constant_name.to_s.start_with?('::') || namespace.nil? || namespace.empty?
|
|
113
|
+
|
|
114
|
+
"#{namespace}::#{normalized_name}"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def source_defined_exception_hierarchy
|
|
118
|
+
@source_defined_exception_hierarchy ||= begin
|
|
119
|
+
definitions = @source_defined_exception_definitions
|
|
120
|
+
defined_names = definitions.map { |definition| definition[:name] }.compact.to_set
|
|
121
|
+
|
|
122
|
+
definitions.each_with_object({}) do |definition, hierarchy|
|
|
123
|
+
next unless definition[:name] && definition[:superclass]
|
|
124
|
+
|
|
125
|
+
superclass_name = if definition[:superclass].to_s.start_with?('::')
|
|
126
|
+
normalize_exception_name(definition[:superclass])
|
|
127
|
+
else
|
|
128
|
+
candidate_name = qualify_source_constant_name(definition[:superclass],
|
|
129
|
+
definition[:namespace])
|
|
130
|
+
if defined_names.include?(candidate_name)
|
|
131
|
+
candidate_name
|
|
132
|
+
else
|
|
133
|
+
normalize_exception_name(definition[:superclass])
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
hierarchy[definition[:name]] ||= superclass_name if superclass_name
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def resolve_exception_constant(exception_name)
|
|
143
|
+
return ::StandardError if exception_name == :standard_error
|
|
144
|
+
return unless exception_name.is_a?(String) && !exception_name.empty?
|
|
145
|
+
|
|
146
|
+
exception_name.split('::').reject(&:empty?).inject(Object) { |scope, const_name| scope.const_get(const_name) }
|
|
147
|
+
rescue NameError
|
|
148
|
+
nil
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def rescue_clause_exception_names(clause_type)
|
|
152
|
+
return [] unless rescue_clause_type?(clause_type)
|
|
153
|
+
|
|
154
|
+
Array(clause_type[1]).filter_map { |exception_name| normalize_exception_name(exception_name) }
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def rescue_clause_exception_constants(clause_type)
|
|
158
|
+
rescue_clause_exception_names(clause_type).filter_map do |exception_name|
|
|
159
|
+
resolve_exception_constant(exception_name)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def exception_constant_covers?(covering_constant, covered_constant)
|
|
164
|
+
return true if covering_constant == covered_constant
|
|
165
|
+
|
|
166
|
+
covered_constant < covering_constant
|
|
167
|
+
rescue StandardError
|
|
168
|
+
false
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def source_defined_exception_covers?(covering_name, covered_name)
|
|
172
|
+
normalized_covering = normalize_exception_name(covering_name)
|
|
173
|
+
current_name = normalize_exception_name(covered_name)
|
|
174
|
+
return false if normalized_covering.nil? || current_name.nil?
|
|
175
|
+
return true if normalized_covering == current_name
|
|
176
|
+
|
|
177
|
+
while (current_name = source_defined_exception_hierarchy[current_name])
|
|
178
|
+
return true if current_name == normalized_covering
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
false
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def exception_name_covers?(covering_name, covered_name)
|
|
185
|
+
covering_constant = resolve_exception_constant(covering_name)
|
|
186
|
+
covered_constant = resolve_exception_constant(covered_name)
|
|
187
|
+
|
|
188
|
+
if covering_constant && covered_constant
|
|
189
|
+
exception_constant_covers?(covering_constant, covered_constant)
|
|
190
|
+
else
|
|
191
|
+
source_defined_exception_covers?(covering_name, covered_name)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def rescue_clause_covers?(covering_clause_type, covered_clause_type)
|
|
196
|
+
return false unless rescue_clause_type?(covering_clause_type) && rescue_clause_type?(covered_clause_type)
|
|
197
|
+
|
|
198
|
+
covering_names = rescue_clause_exception_names(covering_clause_type)
|
|
199
|
+
covered_names = rescue_clause_exception_names(covered_clause_type)
|
|
200
|
+
return false if covering_names.empty? || covered_names.empty?
|
|
201
|
+
|
|
202
|
+
covered_names.all? do |covered_name|
|
|
203
|
+
covering_names.any? do |covering_name|
|
|
204
|
+
exception_name_covers?(covering_name, covered_name)
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def broader_rescue_clause_type_than?(left_clause_type, right_clause_type)
|
|
210
|
+
return false unless rescue_clause_type?(left_clause_type) && rescue_clause_type?(right_clause_type)
|
|
211
|
+
|
|
212
|
+
rescue_clause_covers?(left_clause_type, right_clause_type) &&
|
|
213
|
+
!rescue_clause_covers?(right_clause_type, left_clause_type)
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
# rubocop:enable Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
217
|
+
# rubocop:enable Metrics/AbcSize, Metrics/ClassLength, Metrics/CyclomaticComplexity
|
|
218
|
+
end
|
|
219
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ruby
|
|
4
|
+
module Merge
|
|
5
|
+
# Shared chunk anchors used when preserving project-specific Rake sections.
|
|
6
|
+
module ScaffoldChunkSupport
|
|
7
|
+
ChunkSpec = Struct.new(
|
|
8
|
+
:anchor_type,
|
|
9
|
+
:anchor_value,
|
|
10
|
+
:satellite_patterns,
|
|
11
|
+
:jaccard_threshold,
|
|
12
|
+
:max_lookahead,
|
|
13
|
+
:max_lookbehind,
|
|
14
|
+
keyword_init: true
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
BUNDLER_GEM_TASKS_SPEC = ChunkSpec.new(
|
|
18
|
+
anchor_type: :require_call,
|
|
19
|
+
anchor_value: 'bundler/gem_tasks',
|
|
20
|
+
satellite_patterns: [],
|
|
21
|
+
jaccard_threshold: 0.35,
|
|
22
|
+
max_lookahead: 0,
|
|
23
|
+
max_lookbehind: 0
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
RSPEC_SPEC = ChunkSpec.new(
|
|
27
|
+
anchor_type: :require_call,
|
|
28
|
+
anchor_value: 'rspec/core/rake_task',
|
|
29
|
+
satellite_patterns: ['RSpec::Core::RakeTask.new'],
|
|
30
|
+
jaccard_threshold: 0.35,
|
|
31
|
+
max_lookahead: 5,
|
|
32
|
+
max_lookbehind: 2
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
RUBOCOP_SPEC = ChunkSpec.new(
|
|
36
|
+
anchor_type: :require_call,
|
|
37
|
+
anchor_value: 'rubocop/rake_task',
|
|
38
|
+
satellite_patterns: ['RuboCop::RakeTask.new'],
|
|
39
|
+
jaccard_threshold: 0.35,
|
|
40
|
+
max_lookahead: 5,
|
|
41
|
+
max_lookbehind: 2
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
DEFAULT_TASK_SPEC = ChunkSpec.new(
|
|
45
|
+
anchor_type: :task_call,
|
|
46
|
+
anchor_value: 'default',
|
|
47
|
+
satellite_patterns: [],
|
|
48
|
+
jaccard_threshold: 0.35,
|
|
49
|
+
max_lookahead: 0,
|
|
50
|
+
max_lookbehind: 0
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
ALL_SPECS = [BUNDLER_GEM_TASKS_SPEC, RSPEC_SPEC, RUBOCOP_SPEC, DEFAULT_TASK_SPEC].freeze
|
|
54
|
+
|
|
55
|
+
module_function
|
|
56
|
+
|
|
57
|
+
def jaccard_tokens(text)
|
|
58
|
+
text.scan(/[A-Za-z0-9_]+/).to_set
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def jaccard(a_set, b_set)
|
|
62
|
+
union = a_set | b_set
|
|
63
|
+
return 0.0 if union.empty?
|
|
64
|
+
|
|
65
|
+
(a_set & b_set).size.to_f / union.size
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def task_anchor_match?(source, anchor_value, threshold)
|
|
69
|
+
node_tokens = jaccard_tokens(source.to_s)
|
|
70
|
+
pattern_tokens = jaccard_tokens("task #{anchor_value}")
|
|
71
|
+
jaccard(pattern_tokens, node_tokens) >= threshold
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ruby
|
|
4
|
+
module Merge
|
|
5
|
+
# Stable structural signatures used by Ruby owner and method matching.
|
|
6
|
+
module SignatureSupport
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def method_definition(name, params)
|
|
10
|
+
[:def, name, Array(params)]
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def class_definition(constant_path)
|
|
14
|
+
[:class, constant_path]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def module_definition(constant_path)
|
|
18
|
+
[:module, constant_path]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def singleton_class(expression)
|
|
22
|
+
[:singleton_class, expression]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def constant(name)
|
|
26
|
+
[:const, name]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def variable_assignment(kind, name)
|
|
30
|
+
[kind, name]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def multi_write(targets)
|
|
34
|
+
[:multi_write, Array(targets)]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def conditional(kind, predicate)
|
|
38
|
+
[kind, predicate]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def case_statement(predicate)
|
|
42
|
+
[:case, predicate || '']
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def case_match_statement(predicate)
|
|
46
|
+
[:case_match, predicate || '']
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def loop_statement(kind, *parts)
|
|
50
|
+
[kind, *parts]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def begin_block(first_statement_preview)
|
|
54
|
+
[:begin, first_statement_preview || '']
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def call(method_name, identifier, block: false)
|
|
58
|
+
[block ? :call_with_block : :call, method_name, identifier]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def super_call(block:)
|
|
62
|
+
[:super, block ? :with_block : :no_block]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def forwarding_super_call(block:)
|
|
66
|
+
[:forwarding_super, block ? :with_block : :no_block]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def call_operator_write(write_name, receiver)
|
|
70
|
+
[:call_op_write, write_name, receiver]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def lambda_literal(parameters_source)
|
|
74
|
+
[:lambda, parameters_source || '']
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def execution_block(kind, line_number)
|
|
78
|
+
[kind, line_number]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def parenthesized(first_expression_preview)
|
|
82
|
+
[:parens, first_expression_preview || '']
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def embedded(statements_source)
|
|
86
|
+
[:embedded, statements_source || '']
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def other(class_name, line_number)
|
|
90
|
+
[:other, class_name, line_number]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def textual_method_signature(receiver_prefix, method_name)
|
|
94
|
+
"#{receiver_prefix}#{method_name}"
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
data/lib/ruby/merge/version.rb
CHANGED
|
@@ -5,7 +5,7 @@ module Ruby
|
|
|
5
5
|
# Version namespace for this gem.
|
|
6
6
|
module Version
|
|
7
7
|
# Current gem version.
|
|
8
|
-
VERSION = '7.1.
|
|
8
|
+
VERSION = '7.1.4'
|
|
9
9
|
end
|
|
10
10
|
# Current gem version exposed at the traditional constant location.
|
|
11
11
|
VERSION = Version::VERSION # Traditional Constant Location
|