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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e110369618446503145375467571c154f6f20f6a9a9e2ea1aec9fc87c2557618
|
|
4
|
+
data.tar.gz: ec6e31d2f85b299976139c80f2dee4574d6ef70fb721bcb514b36eddd995a5a8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b15e47b8e03c0da7a237edb9826ea7c05d4cc7c510cbd9c6d31b32d0a4787a2510d869b86cf6bd7c9b9c836bdcd6d4a9b57a921bb679f74d9a665daac2730a4e
|
|
7
|
+
data.tar.gz: c69a81757def5610a400599c3fa9b3459557b5c88c09be084c37e240cdfa00a8d50f7909bf63cf1b30ccc34ab20890d6fa3c6d4d49bca747246340216c7e1b03
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/README.md
CHANGED
|
@@ -267,7 +267,7 @@ If none of the available licenses suit your use case, please [contact us](mailto
|
|
|
267
267
|
[📌gitmoji]: https://gitmoji.dev
|
|
268
268
|
[📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
|
|
269
269
|
[🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
|
|
270
|
-
[🧮kloc-img]: https://img.shields.io/badge/KLOC-
|
|
270
|
+
[🧮kloc-img]: https://img.shields.io/badge/KLOC-2.006-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
|
|
271
271
|
[🔐security]: https://github.com/structuredmerge/structuredmerge-ruby/blob/main/SECURITY.md
|
|
272
272
|
[🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
|
|
273
273
|
[📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ruby
|
|
4
|
+
module Merge
|
|
5
|
+
# Parser-neutral naming policy for a block-local receiver.
|
|
6
|
+
module BlockBindingSupport
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def effective_receiver(receiver, block_var, placeholder: :__block_binding__)
|
|
10
|
+
block_var && receiver == block_var ? placeholder : receiver
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def preferred_block_var(template_var, destination_var)
|
|
14
|
+
template_var if template_var && destination_var && template_var != destination_var
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def merged_block_var(var, preferred_var)
|
|
18
|
+
preferred_var || var
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ruby
|
|
4
|
+
module Merge
|
|
5
|
+
# Detects Ruby comment block directive pairs in source lines.
|
|
6
|
+
#
|
|
7
|
+
# This is Ruby-specific substrate behavior shared by Ruby parser providers.
|
|
8
|
+
# Parser-specific gems may consume the spans and project them into their own
|
|
9
|
+
# node types, but directive token semantics should live here.
|
|
10
|
+
# Directive scanning intentionally keeps the paired-stack validation in one
|
|
11
|
+
# class so every caller receives identical malformed-input diagnostics.
|
|
12
|
+
# rubocop:disable Metrics/AbcSize, Metrics/BlockLength, Metrics/ClassLength
|
|
13
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
14
|
+
class BlockDirectiveDetector
|
|
15
|
+
Span = Struct.new(:kind, :start_line, :end_line, :open_marker, :close_marker, keyword_init: true)
|
|
16
|
+
|
|
17
|
+
NOCOV_TOKEN = ':nocov:'
|
|
18
|
+
SIMPLECOV_DISABLE_RE = /\A\s*#\s*simplecov\s*:\s*disable\b/i
|
|
19
|
+
SIMPLECOV_ENABLE_RE = /\A\s*#\s*simplecov\s*:\s*enable\b/i
|
|
20
|
+
DIRECTIVE_CONTENT_RE = /\A(?::nocov:|[\w-]+:(?:freeze|unfreeze))\z/i
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
def coverage_directive_line?(line, nocov_token: NOCOV_TOKEN)
|
|
24
|
+
stripped = line.to_s.chomp
|
|
25
|
+
stripped.match?(simplecov_disable_re) ||
|
|
26
|
+
stripped.match?(simplecov_enable_re) ||
|
|
27
|
+
stripped.match?(nocov_re(nocov_token))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def directive_content?(content)
|
|
31
|
+
DIRECTIVE_CONTENT_RE.match?(content.to_s.strip) ||
|
|
32
|
+
coverage_directive_line?("# #{content}")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def simplecov_disable_re
|
|
36
|
+
SIMPLECOV_DISABLE_RE
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def simplecov_enable_re
|
|
40
|
+
SIMPLECOV_ENABLE_RE
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def nocov_re(nocov_token = NOCOV_TOKEN)
|
|
44
|
+
/\A\s*#\s?#{Regexp.escape(nocov_token)}\s*\z/i
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def initialize(lines, freeze_token: nil, nocov_token: NOCOV_TOKEN, source_label: nil)
|
|
49
|
+
@lines = lines
|
|
50
|
+
@freeze_token = freeze_token
|
|
51
|
+
@nocov_token = nocov_token
|
|
52
|
+
@source_label = source_label
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def detect_spans
|
|
56
|
+
raw_spans = []
|
|
57
|
+
raw_spans.concat(detect_freeze_spans) if @freeze_token
|
|
58
|
+
raw_spans.concat(detect_nocov_spans)
|
|
59
|
+
|
|
60
|
+
validate_no_crossing(raw_spans.sort_by(&:start_line))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def detector_name
|
|
66
|
+
'ruby-merge'
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def directive_error_class
|
|
70
|
+
Ast::Merge::Error
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def warn_prefix
|
|
74
|
+
if @source_label
|
|
75
|
+
"[#{detector_name}] BlockDirectiveDetector (#{@source_label}):"
|
|
76
|
+
else
|
|
77
|
+
"[#{detector_name}] BlockDirectiveDetector:"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def report_unbalanced(message)
|
|
82
|
+
raise directive_error_class, "#{warn_prefix} #{message}" if @source_label
|
|
83
|
+
|
|
84
|
+
warn("#{warn_prefix} #{message} - ignoring")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def detect_freeze_spans
|
|
88
|
+
freeze_pat = /\A\s*#\s?#{Regexp.escape(@freeze_token)}:freeze\b/i
|
|
89
|
+
unfreeze_pat = /\A\s*#\s?#{Regexp.escape(@freeze_token)}:unfreeze\b/i
|
|
90
|
+
|
|
91
|
+
spans = []
|
|
92
|
+
stack = []
|
|
93
|
+
|
|
94
|
+
@lines.each_with_index do |line, index|
|
|
95
|
+
line_num = index + 1
|
|
96
|
+
stripped = line.to_s.chomp
|
|
97
|
+
if stripped.match?(freeze_pat)
|
|
98
|
+
stack.push({ start_line: line_num, open_marker: stripped })
|
|
99
|
+
elsif stripped.match?(unfreeze_pat)
|
|
100
|
+
if (open = stack.pop)
|
|
101
|
+
spans << Span.new(
|
|
102
|
+
kind: :freeze,
|
|
103
|
+
start_line: open[:start_line],
|
|
104
|
+
end_line: line_num,
|
|
105
|
+
open_marker: open[:open_marker],
|
|
106
|
+
close_marker: stripped
|
|
107
|
+
)
|
|
108
|
+
else
|
|
109
|
+
report_unbalanced("unmatched #{@freeze_token}:unfreeze at line #{line_num}")
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
stack.each do |open|
|
|
115
|
+
report_unbalanced("unclosed #{@freeze_token}:freeze at line #{open[:start_line]}")
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
spans
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def detect_nocov_spans
|
|
122
|
+
return [] unless @nocov_token
|
|
123
|
+
|
|
124
|
+
nocov_pat = self.class.nocov_re(@nocov_token)
|
|
125
|
+
|
|
126
|
+
spans = []
|
|
127
|
+
stack = []
|
|
128
|
+
|
|
129
|
+
@lines.each_with_index do |line, index|
|
|
130
|
+
line_num = index + 1
|
|
131
|
+
stripped = line.to_s.chomp
|
|
132
|
+
if stripped.match?(self.class.simplecov_disable_re)
|
|
133
|
+
stack.push({ start_line: line_num, open_marker: stripped })
|
|
134
|
+
elsif stripped.match?(self.class.simplecov_enable_re)
|
|
135
|
+
if (open = stack.pop)
|
|
136
|
+
spans << Span.new(
|
|
137
|
+
kind: :nocov,
|
|
138
|
+
start_line: open[:start_line],
|
|
139
|
+
end_line: line_num,
|
|
140
|
+
open_marker: open[:open_marker],
|
|
141
|
+
close_marker: stripped
|
|
142
|
+
)
|
|
143
|
+
else
|
|
144
|
+
report_unbalanced("unmatched simplecov:enable at line #{line_num}")
|
|
145
|
+
end
|
|
146
|
+
elsif stripped.match?(nocov_pat)
|
|
147
|
+
if stack.empty?
|
|
148
|
+
stack.push({ start_line: line_num, open_marker: stripped })
|
|
149
|
+
else
|
|
150
|
+
open = stack.pop
|
|
151
|
+
spans << Span.new(
|
|
152
|
+
kind: :nocov,
|
|
153
|
+
start_line: open[:start_line],
|
|
154
|
+
end_line: line_num,
|
|
155
|
+
open_marker: open[:open_marker],
|
|
156
|
+
close_marker: stripped
|
|
157
|
+
)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
stack.each do |open|
|
|
163
|
+
report_unbalanced("unclosed coverage directive at line #{open[:start_line]}")
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
spans
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def validate_no_crossing(spans)
|
|
170
|
+
valid = []
|
|
171
|
+
invalid_indices = Set.new
|
|
172
|
+
|
|
173
|
+
spans.each_with_index do |first, first_index|
|
|
174
|
+
next if invalid_indices.include?(first_index)
|
|
175
|
+
|
|
176
|
+
crossing = false
|
|
177
|
+
spans.each_with_index do |second, second_index|
|
|
178
|
+
next if first_index == second_index || invalid_indices.include?(second_index)
|
|
179
|
+
next unless crossing_spans?(first, second)
|
|
180
|
+
|
|
181
|
+
report_unbalanced(
|
|
182
|
+
"offset-overlapping #{first.kind} block (lines #{first.start_line}..#{first.end_line}) and " \
|
|
183
|
+
"#{second.kind} block (lines #{second.start_line}..#{second.end_line}) - both treated as plain comments"
|
|
184
|
+
)
|
|
185
|
+
invalid_indices.add(first_index)
|
|
186
|
+
invalid_indices.add(second_index)
|
|
187
|
+
crossing = true
|
|
188
|
+
break
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
valid << first unless crossing
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
valid
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def crossing_spans?(first, second)
|
|
198
|
+
first_crosses_second = first.start_line < second.start_line &&
|
|
199
|
+
first.end_line > second.start_line &&
|
|
200
|
+
first.end_line < second.end_line
|
|
201
|
+
second_crosses_first = second.start_line < first.start_line &&
|
|
202
|
+
second.end_line > first.start_line &&
|
|
203
|
+
second.end_line < first.end_line
|
|
204
|
+
first_crosses_second || second_crosses_first
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def top_level_spans_only(spans)
|
|
208
|
+
spans.reject do |span|
|
|
209
|
+
spans.any? do |other|
|
|
210
|
+
other != span &&
|
|
211
|
+
other.start_line <= span.start_line &&
|
|
212
|
+
other.end_line >= span.end_line
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
218
|
+
# rubocop:enable Metrics/AbcSize, Metrics/BlockLength, Metrics/ClassLength
|
|
219
|
+
end
|
|
220
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ruby
|
|
4
|
+
module Merge
|
|
5
|
+
# Ruby-specific doc-comment semantics shared by Ruby parser providers.
|
|
6
|
+
module DocCommentSupport
|
|
7
|
+
TAG_PREFIX = /\A@[a-z_]+\b/
|
|
8
|
+
EXAMPLE_TAG = /\A@example\b(?<rest>.*)\z/
|
|
9
|
+
MAGIC_COMMENT_PREFIXES = %w[
|
|
10
|
+
coding
|
|
11
|
+
encoding
|
|
12
|
+
frozen_string_literal
|
|
13
|
+
shareable_constant_value
|
|
14
|
+
typed
|
|
15
|
+
warn_indent
|
|
16
|
+
].freeze
|
|
17
|
+
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
def normalize_comment_content(raw)
|
|
21
|
+
raw.to_s.sub(/\A\s*#\s?/, '').strip
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def comment_prefix_for(raw)
|
|
25
|
+
raw.to_s[/\A\s*#\s*/] || '# '
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def doc_comment_content?(raw, magic_comment: false)
|
|
29
|
+
content = normalize_comment_content(raw)
|
|
30
|
+
return false if content.empty?
|
|
31
|
+
return false if BlockDirectiveDetector.directive_content?(content)
|
|
32
|
+
return false if magic_comment || magic_comment_content?(content)
|
|
33
|
+
|
|
34
|
+
true
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def magic_comment_content?(content)
|
|
38
|
+
MAGIC_COMMENT_PREFIXES.any? { |prefix| content.to_s.start_with?("#{prefix}:") }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def declared_example_language(rest)
|
|
42
|
+
match = rest.to_s.strip.match(/\A\[(?<language>[^\]]+)\]/)
|
|
43
|
+
normalize_language(match && match[:language])
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def declared_example_language_for_tag(content)
|
|
47
|
+
match = EXAMPLE_TAG.match(content.to_s)
|
|
48
|
+
return unless match
|
|
49
|
+
|
|
50
|
+
declared_example_language(match[:rest])
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def normalize_language(language)
|
|
54
|
+
return if language.nil?
|
|
55
|
+
|
|
56
|
+
normalized = language.to_s.strip.downcase.tr('-', '_')
|
|
57
|
+
return if normalized.empty?
|
|
58
|
+
|
|
59
|
+
normalized
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def next_tag_index(normalized_lines, start_index)
|
|
63
|
+
normalized_lines.each_with_index do |content, index|
|
|
64
|
+
next if index < start_index
|
|
65
|
+
|
|
66
|
+
return index if TAG_PREFIX.match?(content)
|
|
67
|
+
end
|
|
68
|
+
nil
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Example extraction deliberately combines tag scanning and body slicing
|
|
72
|
+
# so the returned indexes remain tied to the original comment entries.
|
|
73
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
74
|
+
def example_blocks(entries)
|
|
75
|
+
normalized = entries.map { |entry| normalize_comment_content(entry[:raw]) }
|
|
76
|
+
normalized.each_with_index.filter_map do |content, tag_index|
|
|
77
|
+
match = EXAMPLE_TAG.match(content)
|
|
78
|
+
next unless match
|
|
79
|
+
|
|
80
|
+
body_start_index = tag_index + 1
|
|
81
|
+
body_end_index = next_tag_index(normalized, body_start_index) || normalized.length
|
|
82
|
+
next if body_start_index >= body_end_index
|
|
83
|
+
|
|
84
|
+
body_entries = entries[body_start_index...body_end_index]
|
|
85
|
+
next if body_entries.nil? || body_entries.empty?
|
|
86
|
+
|
|
87
|
+
{
|
|
88
|
+
tag_index: tag_index,
|
|
89
|
+
tag_line: entries[tag_index][:line],
|
|
90
|
+
tag_text: normalized[tag_index],
|
|
91
|
+
body_start_index: body_start_index,
|
|
92
|
+
body_end_index: body_end_index,
|
|
93
|
+
body_entries: body_entries,
|
|
94
|
+
declared_language: declared_example_language(match[:rest])
|
|
95
|
+
}
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'block_binding_support'
|
|
4
|
+
|
|
5
|
+
module Ruby
|
|
6
|
+
module Merge
|
|
7
|
+
# Shared gemspec block-variable normalization for Ruby templating.
|
|
8
|
+
module GemspecSupport
|
|
9
|
+
GEMSPEC_VAR_PLACEHOLDER = :__gemspec_var__
|
|
10
|
+
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def effective_receiver(receiver, gemspec_block_var)
|
|
14
|
+
BlockBindingSupport.effective_receiver(receiver, gemspec_block_var, placeholder: GEMSPEC_VAR_PLACEHOLDER)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def preferred_block_var(template_var, dest_var)
|
|
18
|
+
BlockBindingSupport.preferred_block_var(template_var, dest_var)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def merged_block_var(var, preferred_var)
|
|
22
|
+
BlockBindingSupport.merged_block_var(var, preferred_var)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def opening_line_with_preferred_block_var(opening_line, dest_var:, preferred_var:, node_preference:)
|
|
26
|
+
return opening_line unless preferred_var && dest_var && dest_var != preferred_var
|
|
27
|
+
return opening_line unless node_preference == :destination
|
|
28
|
+
|
|
29
|
+
opening_line.sub("|#{dest_var}|", "|#{preferred_var}|")
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ruby
|
|
4
|
+
module Merge
|
|
5
|
+
# Ruby magic comment detection and file-header prefix handling.
|
|
6
|
+
# Header scanning intentionally preserves source order and duplicate entries
|
|
7
|
+
# because those details affect comment ownership during reconstruction.
|
|
8
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
9
|
+
module MagicCommentSupport
|
|
10
|
+
MAGIC_COMMENT_PATTERNS = {
|
|
11
|
+
frozen_string_literal: /^frozen_string_literal:\s*(true|false)$/i,
|
|
12
|
+
encoding: /^(encoding|coding):\s*\S+$/i,
|
|
13
|
+
warn_indent: /^warn_indent:\s*(true|false)$/i,
|
|
14
|
+
shareable_constant_value: /^shareable_constant_value:\s*\S+$/i
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
def magic_comment_type_for_text(text)
|
|
20
|
+
stripped = DocCommentSupport.normalize_comment_content(text)
|
|
21
|
+
|
|
22
|
+
MAGIC_COMMENT_PATTERNS.each do |type, pattern|
|
|
23
|
+
return type if stripped.match?(pattern)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def comment_only_prefix_info(lines)
|
|
30
|
+
entries = []
|
|
31
|
+
suppressed_line_nums = Set.new
|
|
32
|
+
duplicate_magic_line_nums = Set.new
|
|
33
|
+
|
|
34
|
+
if shebang_line?(lines.first)
|
|
35
|
+
entries << { line_num: 1, text: lines.first.to_s, kind: :shebang }
|
|
36
|
+
suppressed_line_nums << 1
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
header_magic_types = header_magic_comment_types_for_lines(lines)
|
|
40
|
+
seen_magic_types = Set.new
|
|
41
|
+
|
|
42
|
+
header_magic_types.keys.sort.each do |line_num|
|
|
43
|
+
magic_type = header_magic_types[line_num]
|
|
44
|
+
suppressed_line_nums << line_num
|
|
45
|
+
entries << { line_num: line_num, text: lines[line_num - 1].to_s.chomp, kind: :magic }
|
|
46
|
+
|
|
47
|
+
if seen_magic_types.include?(magic_type)
|
|
48
|
+
duplicate_magic_line_nums << line_num
|
|
49
|
+
else
|
|
50
|
+
seen_magic_types << magic_type
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
if header_magic_types.any?
|
|
55
|
+
blank_line_num = header_magic_types.keys.max + 1
|
|
56
|
+
|
|
57
|
+
while blank_line_num <= lines.length && lines[blank_line_num - 1].to_s.rstrip.empty?
|
|
58
|
+
entries << { line_num: blank_line_num, text: lines[blank_line_num - 1].to_s, kind: :blank }
|
|
59
|
+
suppressed_line_nums << blank_line_num
|
|
60
|
+
blank_line_num += 1
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
{
|
|
65
|
+
entries: entries,
|
|
66
|
+
suppressed_line_nums: suppressed_line_nums,
|
|
67
|
+
duplicate_magic_line_nums: duplicate_magic_line_nums,
|
|
68
|
+
header_magic_comment_types: header_magic_types
|
|
69
|
+
}
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def header_magic_comment_types_for_lines(lines)
|
|
73
|
+
types = {}
|
|
74
|
+
index = shebang_line?(lines.first) ? 1 : 0
|
|
75
|
+
previous_line_num = shebang_line?(lines.first) ? 1 : nil
|
|
76
|
+
|
|
77
|
+
while index < lines.length
|
|
78
|
+
line_num = index + 1
|
|
79
|
+
stripped = lines[index].to_s.rstrip
|
|
80
|
+
break if stripped.empty?
|
|
81
|
+
|
|
82
|
+
expected_line_num = previous_line_num ? previous_line_num + 1 : 1
|
|
83
|
+
break unless line_num == expected_line_num
|
|
84
|
+
|
|
85
|
+
magic_type = magic_comment_type_for_text(stripped)
|
|
86
|
+
break unless magic_type
|
|
87
|
+
|
|
88
|
+
types[line_num] = magic_type
|
|
89
|
+
previous_line_num = line_num
|
|
90
|
+
index += 1
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
types
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def prefix_comment_line_numbers_for_comments(comments)
|
|
97
|
+
prefix_line_nums = Set.new
|
|
98
|
+
previous_line_num = nil
|
|
99
|
+
index = 0
|
|
100
|
+
|
|
101
|
+
if shebang_comment?(comments.first)
|
|
102
|
+
prefix_line_nums << 1
|
|
103
|
+
previous_line_num = 1
|
|
104
|
+
index = 1
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
while index < comments.length
|
|
108
|
+
comment = comments[index]
|
|
109
|
+
line_num = comment.location.start_line
|
|
110
|
+
expected_line_num = previous_line_num ? previous_line_num + 1 : 1
|
|
111
|
+
break unless line_num == expected_line_num
|
|
112
|
+
break unless magic_comment_type_for_text(comment.slice)
|
|
113
|
+
|
|
114
|
+
prefix_line_nums << line_num
|
|
115
|
+
previous_line_num = line_num
|
|
116
|
+
index += 1
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
prefix_line_nums
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def shebang_line?(line)
|
|
123
|
+
line.to_s.start_with?('#!')
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def shebang_comment?(comment)
|
|
127
|
+
comment&.slice&.start_with?('#!')
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ruby
|
|
4
|
+
module Merge
|
|
5
|
+
# Computes stable name/parameter similarity scores for Ruby methods.
|
|
6
|
+
#
|
|
7
|
+
# The dynamic-programming distance calculation is intentionally kept local
|
|
8
|
+
# to this value object so the matching contract stays easy to audit.
|
|
9
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
10
|
+
class MethodSimilarity
|
|
11
|
+
DEFAULT_NAME_WEIGHT = 0.7
|
|
12
|
+
DEFAULT_PARAMS_WEIGHT = 0.3
|
|
13
|
+
|
|
14
|
+
attr_reader :name_weight, :params_weight
|
|
15
|
+
|
|
16
|
+
def initialize(name_weight: DEFAULT_NAME_WEIGHT, params_weight: DEFAULT_PARAMS_WEIGHT)
|
|
17
|
+
@name_weight = name_weight
|
|
18
|
+
@params_weight = params_weight
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def call(template_name:, template_params:, dest_name:, dest_params:)
|
|
22
|
+
name_score = string_similarity(template_name.to_s, dest_name.to_s)
|
|
23
|
+
param_score = param_similarity(Array(template_params), Array(dest_params))
|
|
24
|
+
|
|
25
|
+
(name_score * name_weight) + (param_score * params_weight)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def param_similarity(template_params, dest_params)
|
|
29
|
+
return 1.0 if template_params.empty? && dest_params.empty?
|
|
30
|
+
return 0.0 if template_params.empty? || dest_params.empty?
|
|
31
|
+
|
|
32
|
+
common = (template_params & dest_params).size
|
|
33
|
+
total = [template_params.size, dest_params.size].max
|
|
34
|
+
count_ratio = [template_params.size, dest_params.size].min.to_f / total
|
|
35
|
+
name_match_ratio = common.to_f / total
|
|
36
|
+
|
|
37
|
+
(name_match_ratio * 0.7) + (count_ratio * 0.3)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def string_similarity(str1, str2)
|
|
41
|
+
return 1.0 if str1 == str2
|
|
42
|
+
return 0.0 if str1.empty? || str2.empty?
|
|
43
|
+
|
|
44
|
+
distance = levenshtein_distance(str1, str2)
|
|
45
|
+
max_len = [str1.length, str2.length].max
|
|
46
|
+
|
|
47
|
+
1.0 - (distance.to_f / max_len)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def levenshtein_distance(str1, str2)
|
|
51
|
+
return str2.length if str1.empty?
|
|
52
|
+
return str1.length if str2.empty?
|
|
53
|
+
|
|
54
|
+
str1, str2 = str2, str1 if str1.length > str2.length
|
|
55
|
+
|
|
56
|
+
m = str1.length
|
|
57
|
+
n = str2.length
|
|
58
|
+
previous_row = (0..m).to_a
|
|
59
|
+
current_row = Array.new(m + 1, 0)
|
|
60
|
+
|
|
61
|
+
(1..n).each do |j|
|
|
62
|
+
current_row[0] = j
|
|
63
|
+
|
|
64
|
+
(1..m).each do |i|
|
|
65
|
+
cost = str1[i - 1] == str2[j - 1] ? 0 : 1
|
|
66
|
+
current_row[i] = [
|
|
67
|
+
previous_row[i] + 1,
|
|
68
|
+
current_row[i - 1] + 1,
|
|
69
|
+
previous_row[i - 1] + cost
|
|
70
|
+
].min
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
previous_row, current_row = current_row, previous_row
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
previous_row[m]
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ruby
|
|
4
|
+
module Merge
|
|
5
|
+
# Shared Ruby synthetic node for a balanced coverage-exclusion block.
|
|
6
|
+
#
|
|
7
|
+
# Parser-specific merge gems subclass this when they need native AST location
|
|
8
|
+
# or comment attachment behavior. The structural Ruby semantics live here:
|
|
9
|
+
# nocov blocks follow file preference and match by their inner content.
|
|
10
|
+
# Nocov nodes preserve source locations and marker text as one structural
|
|
11
|
+
# unit; the initializer mirrors the node's complete serialized shape.
|
|
12
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/ParameterLists, Metrics/PerceivedComplexity
|
|
13
|
+
class NocovNodeBase
|
|
14
|
+
include Ast::Merge::BlockDirective
|
|
15
|
+
|
|
16
|
+
InvalidStructureError = Class.new(StandardError)
|
|
17
|
+
|
|
18
|
+
Location = Struct.new(:start_line, :end_line) do
|
|
19
|
+
def cover?(line)
|
|
20
|
+
(start_line..end_line).cover?(line)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
attr_reader :start_line, :end_line, :nodes, :analysis, :start_marker, :close_marker
|
|
25
|
+
|
|
26
|
+
def initialize(start_line:, end_line:, analysis:, nodes: [], start_marker: nil, close_marker: nil)
|
|
27
|
+
@start_line = start_line
|
|
28
|
+
@end_line = end_line
|
|
29
|
+
@analysis = analysis
|
|
30
|
+
@nodes = nodes
|
|
31
|
+
@start_marker = start_marker
|
|
32
|
+
@close_marker = close_marker
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def kind = :nocov
|
|
36
|
+
|
|
37
|
+
def children = @nodes
|
|
38
|
+
|
|
39
|
+
def merge_policy = nil
|
|
40
|
+
|
|
41
|
+
def location
|
|
42
|
+
@location ||= Location.new(@start_line, @end_line)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def slice
|
|
46
|
+
return unless @analysis
|
|
47
|
+
|
|
48
|
+
lines = @analysis.lines
|
|
49
|
+
return unless lines
|
|
50
|
+
|
|
51
|
+
lines[(@start_line - 1)..(@end_line - 1)]&.join
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def signature
|
|
55
|
+
return [:NocovNode, nil] if @nodes.empty? || @analysis.nil?
|
|
56
|
+
|
|
57
|
+
if @nodes.length == 1
|
|
58
|
+
@analysis.generate_signature(@nodes.first)
|
|
59
|
+
else
|
|
60
|
+
inner_lines = @analysis.lines && @analysis.lines[@start_line..(@end_line - 2)]
|
|
61
|
+
[:nocov_multi, inner_lines&.map(&:strip)&.join("\n")]
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def merge_type = :nocov_block
|
|
66
|
+
|
|
67
|
+
alias type merge_type
|
|
68
|
+
|
|
69
|
+
def nocov_node? = true
|
|
70
|
+
|
|
71
|
+
def inspect
|
|
72
|
+
"#<#{self.class} lines=#{@start_line}..#{@end_line} nodes=#{@nodes.length}>"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
alias to_s inspect
|
|
76
|
+
end
|
|
77
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/ParameterLists, Metrics/PerceivedComplexity
|
|
78
|
+
end
|
|
79
|
+
end
|