prism-merge 7.1.9 → 7.1.10

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 12db87972f5ea68989cbdc9d7ee3a1e1b53d76b88bd1e80eb51f1049c9c5eaa3
4
- data.tar.gz: 7c59aba98025df06fa3cc8829d67865be80312c6ac60fce7b9d3762b9e9f50ff
3
+ metadata.gz: 0311a16d60cb4d5bb9ad04d1223c05e85aa80056a4d14f00087f6d0fed6c2909
4
+ data.tar.gz: f65258b654893b9be95415ebfe05c0f634b3a92273c81d0169ee29b1584ef068
5
5
  SHA512:
6
- metadata.gz: 7d88966f36038245dc0d96e37df4bdec785a82309ec3509655540cd6a6e6d881c46b5c3ce86918e036af9aa9cb11ead717318d72a09b13a9ec217b5940f54cae
7
- data.tar.gz: 53bf4b9c022a78181562383f35c6bb2539c9de40232ca11d20de07d4e0976900466814c781a8ff556231ca4c1bba67d262f63a1bcdf767c44833855ffa2c8c65
6
+ metadata.gz: 2c2f70118664e21623d068196cf057afe65f535f54442f1b03957a9a5a126a0809ad24235a6c39b94945267d1f2220788bd37696098ba7e2b12856202a7433ff
7
+ data.tar.gz: 32592677ef4e368db093730cc25064c532e4d02e10bf3f149a39ca46c5bcf47386b315fc1c3f396c7f87820883820c575607c4d23062b1d75f12618e97b2fec4
checksums.yaml.gz.sig CHANGED
Binary file
@@ -49,6 +49,21 @@ module Prism
49
49
  #
50
50
  # For non-magic blocks, uses the parent implementation.
51
51
  #
52
+ # == NOT PART OF PRAGMA RESOLUTION
53
+ #
54
+ # This signature makes a comment-only file's magic comment matchable as a
55
+ # top-level node. That is the mechanism by which a file-level pragma can be
56
+ # treated as ordinary content and emitted at the wrong position: when the
57
+ # destination is comment-only, its pragma becomes a node whose signature
58
+ # cannot match the template's pragma (which is a leading comment of the
59
+ # template's first statement, not a statement), so it lands after the last
60
+ # template statement where Ruby ignores it.
61
+ #
62
+ # Do NOT extend pragma handling here. Which pragmas survive a merge, and
63
+ # where they are placed, is decided by {Prism::Merge::PragmaMerger}, wired
64
+ # in from {Prism::Merge::SmartMerger#apply_pragma_header} after this
65
+ # resolver runs. This signature governs comment-block MATCHING only.
66
+ #
52
67
  # @return [Array] Signature for matching
53
68
  def signature
54
69
  if contains_magic_comment?
@@ -58,6 +58,20 @@ module Prism
58
58
  # - `# warn_indent: true/false`
59
59
  # - `# shareable_constant_value: literal/...`
60
60
  #
61
+ # == CLASSIFICATION ONLY, NOT PRAGMA RESOLUTION
62
+ #
63
+ # This attribute feeds comment classification and node signatures. It does
64
+ # not decide whether a pragma survives a merge or where it is placed - that
65
+ # is {Prism::Merge::PragmaMerger}, wired in from
66
+ # {Prism::Merge::SmartMerger#apply_pragma_header}. PragmaMerger reads
67
+ # Prism's native ParseResult#magic_comments directly and does not consult
68
+ # this attribute.
69
+ #
70
+ # Do not build pragma retention or placement on top of this. Note also that
71
+ # `magic_comment_type` is populated only for comments in the header prefix,
72
+ # so it is not a reliable test for "is this text a pragma" elsewhere in a
73
+ # file.
74
+ #
61
75
  # @return [Boolean] true if this is a magic comment
62
76
  def magic_comment?
63
77
  !@magic_comment_type.nil?
@@ -33,6 +33,10 @@ module Prism
33
33
 
34
34
  nodes = []
35
35
  current_block = []
36
+ # CLASSIFICATION ONLY: tags comment lines so Comment::Line can carry a
37
+ # magic_comment_type for node signatures. Not pragma resolution - see
38
+ # Prism::Merge::PragmaMerger, applied by
39
+ # Prism::Merge::SmartMerger#apply_pragma_header after the merge.
36
40
  header_magic_comment_types = Prism::Merge::MagicCommentSupport.header_magic_comment_types_for_lines(lines)
37
41
 
38
42
  lines.each_with_index do |line, idx|
@@ -2,6 +2,28 @@
2
2
 
3
3
  module Prism
4
4
  module Merge
5
+ # Merges files whose top-level statements are ALL comments (no code).
6
+ #
7
+ # Dispatched from TopLevelMergeRunner#comment_only_merge?, which requires BOTH
8
+ # sides to be comment-only. A mixed pair - e.g. a comment-only destination
9
+ # merged into a template that has code - takes the main node path instead.
10
+ #
11
+ # == NOT THE PRAGMA RESOLVER
12
+ #
13
+ # The header handling here (comment_only_prefix_info, duplicate_magic_line_nums,
14
+ # emit_comment_only_prefix_lines) deduplicates magic comments WITHIN one side's
15
+ # own header prefix. It does not resolve pragmas ACROSS template and
16
+ # destination, and it does not run at all for a mixed pair.
17
+ #
18
+ # That gap is why a comment-only destination's pragma used to be emitted as an
19
+ # unmatched node at the end of the merged file, where Ruby ignores it. Which
20
+ # pragmas survive a merge and where they are placed is decided by
21
+ # {Prism::Merge::PragmaMerger}, wired in from
22
+ # {Prism::Merge::SmartMerger#apply_pragma_header} after the resolver runs, so it
23
+ # covers both dispatch paths uniformly.
24
+ #
25
+ # Keep this class focused on comment-only files. Do not extend it to resolve
26
+ # pragma retention or placement.
5
27
  class CommentOnlyFileMerger
6
28
  attr_reader :merger, :comment_only_prefix_lines
7
29
 
@@ -809,6 +809,17 @@ module Prism
809
809
  }
810
810
  end
811
811
 
812
+ # Magic comment types in this file's header prefix, keyed by line number.
813
+ #
814
+ # CLASSIFICATION ONLY. Used to populate Comment::Line#magic_comment_type so
815
+ # comment nodes can be typed and signed. Not pragma resolution: it does not
816
+ # decide which pragmas survive a merge or where they are placed. See
817
+ # Prism::Merge::PragmaMerger (wired in via
818
+ # Prism::Merge::SmartMerger#apply_pragma_header) for that.
819
+ #
820
+ # Note this is regex/line-based (via MagicCommentSupport) and only covers the
821
+ # contiguous header prefix - it breaks at the first blank line. PragmaMerger
822
+ # instead reads Prism's native ParseResult#magic_comments.
812
823
  def native_header_magic_comment_types
813
824
  @native_header_magic_comment_types ||= Prism::Merge::MagicCommentSupport.header_magic_comment_types_for_lines(@lines)
814
825
  end
@@ -3,6 +3,49 @@
3
3
  module Prism
4
4
  module Merge
5
5
  # Compatibility wrapper for Ruby substrate magic-comment support.
6
+ #
7
+ # == THIS IS NOT THE PRAGMA RESOLVER
8
+ #
9
+ # Do not add new magic-comment / pragma handling here, or in any of the
10
+ # callers listed below. This module only classifies comment TEXT and reports
11
+ # header line numbers. It does not decide which pragmas survive a merge, nor
12
+ # where they are placed.
13
+ #
14
+ # The authoritative resolver for file-level pragmas is
15
+ # {Prism::Merge::PragmaMerger}, wired in from
16
+ # {Prism::Merge::SmartMerger#apply_pragma_header}. Pragma semantics live there:
17
+ #
18
+ # KEPT - preference resolved exactly like ordinary content, honouring
19
+ # add_template_only_nodes and remove_template_missing_nodes
20
+ # POSITION - always pinned to the header region (below any shebang), because
21
+ # Ruby ignores a magic comment that appears after the first
22
+ # statement
23
+ #
24
+ # Anything built on this module instead will silently reintroduce the two
25
+ # defects PragmaMerger exists to prevent: a pragma stranded below code (inert,
26
+ # Lint/MisplacedMagicComment) and a pragma dropped because retention followed
27
+ # `preference` alone rather than the add/remove flags.
28
+ #
29
+ # == Remaining consumers (legacy, classification only)
30
+ #
31
+ # These still consult this module. Each is a COMMENT CLASSIFICATION or node
32
+ # SIGNATURE concern, not pragma retention or placement, and none may grow
33
+ # pragma handling:
34
+ #
35
+ # lib/prism/merge/comment/line.rb magic_comment_type attribute
36
+ # lib/prism/merge/comment/block.rb contains_magic_comment?, magic_comments
37
+ # lib/prism/merge/comment/parser.rb header classification while parsing
38
+ # lib/prism/merge/file_analysis.rb native_header_magic_comment_types
39
+ # lib/prism/merge/comment_only_file_merger.rb comment-only file header handling
40
+ # lib/prism/merge/node_emission_support.rb leading-comment prefix emission
41
+ # lib/prism/merge/recursive_node_body_merger.rb prefix comment filtering
42
+ # lib/prism/merge/wrapper_comment_support.rb leading-comment emission
43
+ # lib/prism/merge/ruby_doc_surface_analyzer.rb doc-comment vs directive split
44
+ # lib/prism/merge.rb prism_comment_directive classification
45
+ #
46
+ # Removing them is deliberately deferred: Comment::Line#magic_comment_type
47
+ # feeds node signatures, so deleting it changes match identity well beyond
48
+ # pragmas. That work needs its own pass with the full monorepo suite as guard.
6
49
  module MagicCommentSupport
7
50
  module_function
8
51
 
@@ -2,6 +2,24 @@
2
2
 
3
3
  module Prism
4
4
  module Merge
5
+ # Emits nodes and their comments into the MergeResult.
6
+ #
7
+ # == PRAGMAS ARE NOT PLACED HERE
8
+ #
9
+ # The prefix/leading-comment emission below positions ordinary comments. A
10
+ # file-level pragma (magic comment) is NOT placed by this class, and must not
11
+ # be: Ruby honours a pragma only when it precedes the first statement, so its
12
+ # position cannot depend on which node happens to carry it.
13
+ #
14
+ # Pragma retention and placement are resolved by {Prism::Merge::PragmaMerger},
15
+ # applied once by {Prism::Merge::SmartMerger#apply_pragma_header} after the
16
+ # whole merge finishes. That post-pass strips pragmas from wherever emission
17
+ # put them and pins the resolved block to the header region, so it is correct
18
+ # regardless of the path taken here.
19
+ #
20
+ # Consequence: a pragma may transiently appear at the wrong position in the
21
+ # result this class builds. That is expected and repaired downstream. Do not
22
+ # add pragma-specific handling here to pre-empt it.
5
23
  class NodeEmissionSupport
6
24
  include Prism::Merge::SourceLineLookup
7
25
 
@@ -0,0 +1,349 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prism
4
+ module Merge
5
+ # Merges file-level pragmas as a discrete document.
6
+ #
7
+ # == Why a dedicated resolver
8
+ #
9
+ # A magic comment is syntactically a comment but semantically a file-level
10
+ # directive, and the two axes of a merge resolve differently for it:
11
+ #
12
+ # KEPT - preference resolved, exactly like any other content. Either
13
+ # side may add or remove a pragma, governed by the same
14
+ # `preference`, `add_template_only_nodes` and
15
+ # `remove_template_missing_nodes` settings that govern code.
16
+ # POSITION - never follows merge placement rules. Ruby only honours a magic
17
+ # comment that appears in the leading comment region, before the
18
+ # first statement; one emitted after code is inert. So a pragma
19
+ # must always be pinned to the top.
20
+ #
21
+ # Modelling a pragma as an ordinary comment attached to a statement satisfies
22
+ # neither axis: it inherits the host statement's placement (so it can be
23
+ # stranded below code, silently disabling e.g. frozen string literals), and it
24
+ # bypasses the add/remove flags (which operate on statements), so retention
25
+ # follows `preference` alone.
26
+ #
27
+ # Treating the pragma region as its own document resolves both at once:
28
+ # retention is computed here by the same rule code uses, and the rendered block
29
+ # is pinned above the first statement, so it cannot be misplaced.
30
+ #
31
+ # == Scope
32
+ #
33
+ # Only file-level pragmas are handled here. Prism classifies ANY leading
34
+ # `# key: value` as a magic comment, which includes block directives such as
35
+ # `# kettle-jem:freeze` and `# simplecov:disable`. Those are positionally
36
+ # meaningful and legitimately appear mid-file, so they are excluded by key and
37
+ # remain the main resolver's concern.
38
+ #
39
+ # Identity and position come from Prism natively (ParseResult#magic_comments
40
+ # with key, value and value_loc), so no text pattern matching is involved.
41
+ #
42
+ # == What this REPLACES
43
+ #
44
+ # Magic-comment handling elsewhere in this gem is legacy and is classification
45
+ # only. It must not grow pragma retention or placement logic. Each such site
46
+ # carries a "NOT THE PRAGMA RESOLVER" / "CLASSIFICATION ONLY" marker pointing
47
+ # back here:
48
+ #
49
+ # magic_comment_support.rb regex/line-based header classification
50
+ # comment/line.rb magic_comment_type attribute (feeds signatures)
51
+ # comment/block.rb signature making a pragma matchable as a node
52
+ # comment/parser.rb header tagging during parse
53
+ # file_analysis.rb native_header_magic_comment_types
54
+ # comment_only_file_merger.rb within-one-side header dedup only
55
+ # node_emission_support.rb leading-comment prefix emission
56
+ # wrapper_comment_support.rb leading-comment emission
57
+ # recursive_node_body_merger.rb prefix comment filtering
58
+ # ruby_doc_surface_analyzer.rb doc-vs-directive split
59
+ # merge.rb prism_comment_directive classification
60
+ #
61
+ # Removing them is deliberately deferred: Comment::Line#magic_comment_type feeds
62
+ # node signatures, so deleting it changes match identity well beyond pragmas.
63
+ # That needs its own pass with the full monorepo suite as guard.
64
+ #
65
+ # @example
66
+ # merger = PragmaMerger.new(
67
+ # template_source: template,
68
+ # destination_source: destination,
69
+ # preference: :destination,
70
+ # add_template_only_nodes: true,
71
+ # remove_template_missing_nodes: false
72
+ # )
73
+ # merger.resolved_entries # => [#<Entry key="frozen_string_literal" value="true" line=1>]
74
+ # merger.render # => "# frozen_string_literal: true\n"
75
+ # merger.place_in(body) # => pragma block pinned above the first statement
76
+ #
77
+ # @example Removing pragmas the node resolver emitted
78
+ # PragmaMerger.strip_pragma_lines("# frozen_string_literal: true\nx = 1\n")
79
+ # # => "x = 1\n"
80
+ # rubocop:disable Metrics/ClassLength -- the retention rule, the rendering of the
81
+ # pragma block, its header placement, and the AST-derived line scan form one
82
+ # boundary: splitting them across classes would separate the two merge axes
83
+ # (KEPT and POSITION) that must stay resolved together.
84
+ class PragmaMerger
85
+ # File-level pragmas, as opposed to block directives. Filtering happens on
86
+ # the AST-derived key (Set membership), never by matching comment text.
87
+ PRAGMA_KEYS = %w[
88
+ frozen_string_literal
89
+ encoding
90
+ coding
91
+ warn_indent
92
+ shareable_constant_value
93
+ ].to_set.freeze
94
+
95
+ # A single pragma occurrence.
96
+ #
97
+ # @!attribute key
98
+ # @return [String] pragma name, e.g. "frozen_string_literal"
99
+ # @!attribute value
100
+ # @return [String] pragma value, e.g. "true"
101
+ # @!attribute line
102
+ # @return [Integer] 1-based source line the pragma occupies
103
+ Entry = Struct.new(:key, :value, :line, keyword_init: true) do
104
+ # Renders the pragma in canonical `# key: value` form.
105
+ #
106
+ # @return [String]
107
+ def to_line
108
+ "# #{key}: #{value}"
109
+ end
110
+ end
111
+
112
+ # @param template_source [String]
113
+ # @param destination_source [String]
114
+ # @param preference [Symbol] :destination or :template
115
+ # @param add_template_only_nodes [Boolean]
116
+ # @param remove_template_missing_nodes [Boolean]
117
+ def initialize(template_source:, destination_source:, preference: :destination,
118
+ add_template_only_nodes: false, remove_template_missing_nodes: false)
119
+ @template_source = template_source.to_s
120
+ @destination_source = destination_source.to_s
121
+ @preference = preference.to_sym
122
+ @add_template_only_nodes = add_template_only_nodes
123
+ @remove_template_missing_nodes = remove_template_missing_nodes
124
+ end
125
+
126
+ attr_reader :preference
127
+
128
+ # Pragma entries declared by the template, in source order.
129
+ #
130
+ # @return [Array<Entry>]
131
+ def template_entries
132
+ @template_entries ||= entries_for(@template_source)
133
+ end
134
+
135
+ # Pragma entries declared by the destination, in source order.
136
+ #
137
+ # @return [Array<Entry>]
138
+ def destination_entries
139
+ @destination_entries ||= entries_for(@destination_source)
140
+ end
141
+
142
+ # The pragmas to keep, in render order.
143
+ #
144
+ # Retention mirrors how ordinary nodes resolve:
145
+ # declared by both -> kept, value resolved by preference
146
+ # template only -> kept iff preference is :template, or
147
+ # add_template_only_nodes is set
148
+ # destination only -> kept iff remove_template_missing_nodes is unset
149
+ #
150
+ # Order is deterministic: template order first, then any destination-only
151
+ # pragma in destination order. The template is the authority on header
152
+ # structure; ordering is not itself preference resolved because neither side
153
+ # expresses an ordering preference for a region it may not declare at all.
154
+ #
155
+ # @return [Array<Entry>]
156
+ def resolved_entries
157
+ @resolved_entries ||= compute_resolved_entries
158
+ end
159
+
160
+ # True when at least one pragma survives the merge.
161
+ #
162
+ # @return [Boolean]
163
+ def any?
164
+ resolved_entries.any?
165
+ end
166
+
167
+ # Renders the merged pragma block, one pragma per line, no trailing newline
168
+ # separation logic (callers place it in the header region).
169
+ #
170
+ # @return [String] empty when no pragma is kept
171
+ def render
172
+ return '' if resolved_entries.empty?
173
+
174
+ resolved_entries.map { |entry| "#{entry.to_line}\n" }.join
175
+ end
176
+
177
+ # Places the merged pragma block into +content+, honouring the two
178
+ # positional constraints Ruby imposes:
179
+ #
180
+ # * a shebang must remain the very first line, so pragmas go below it
181
+ # * pragmas must precede the first statement, so they go above all code
182
+ #
183
+ # A blank line separates the block from whatever follows, matching normal
184
+ # file layout. Returns +content+ unchanged when no pragma is kept.
185
+ #
186
+ # @param content [String] merged body with pragma lines already removed
187
+ # @return [String]
188
+ def place_in(content)
189
+ block = render
190
+ return content.to_s if block.empty?
191
+
192
+ text = content.to_s
193
+ lines = text.lines
194
+ shebang = lines.first.to_s.start_with?('#!') ? lines.shift : nil
195
+ prefix = [shebang, block].compact
196
+ rest = drop_leading_blank_lines(lines)
197
+ [*prefix, "\n", *rest].join
198
+ end
199
+
200
+ # Removes pragma lines from +source+, leaving every other line untouched.
201
+ #
202
+ # A line is only removed when the pragma is the whole line. A pragma sharing
203
+ # a line with code is left alone rather than risk corrupting the statement,
204
+ # which cannot happen for a recognised magic comment in any case.
205
+ #
206
+ # @param source [String]
207
+ # @return [String]
208
+ def self.strip_pragma_lines(source)
209
+ text = source.to_s
210
+ return text if text.empty?
211
+
212
+ lines_to_drop = pragma_line_numbers(text)
213
+ return text if lines_to_drop.empty?
214
+
215
+ text.lines.each_with_index.reject { |_, index| lines_to_drop.include?(index + 1) }.map(&:first).join
216
+ end
217
+
218
+ # Line numbers (1-based) occupied by a file-level pragma in +source+.
219
+ #
220
+ # @param source [String]
221
+ # @return [Set<Integer>]
222
+ def self.pragma_line_numbers(source)
223
+ parse = Prism.parse(source.to_s)
224
+ lines = source.to_s.lines
225
+ pragma_magic_comments(parse)
226
+ .filter_map { |magic_comment| pragma_line(magic_comment, lines) }
227
+ .to_set
228
+ end
229
+
230
+ # Prism's own magic comments, narrowed to the file-level pragma keys.
231
+ def self.pragma_magic_comments(parse)
232
+ parse.magic_comments.select { |magic_comment| PRAGMA_KEYS.include?(magic_comment.key.to_s) }
233
+ end
234
+
235
+ # The 1-based line a pragma occupies, or nil when the pragma shares its line
236
+ # with code. Only wholly-comment lines are reported, so stripping one cannot
237
+ # take code with it.
238
+ def self.pragma_line(magic_comment, lines)
239
+ line = magic_comment.value_loc.start_line
240
+ comment_only_line?(lines[line - 1]) ? line : nil
241
+ end
242
+
243
+ def self.comment_only_line?(line)
244
+ line.to_s.strip.start_with?('#')
245
+ end
246
+
247
+ private
248
+
249
+ def compute_resolved_entries
250
+ by_key = merge_entry_values
251
+ apply_template_retention(by_key)
252
+ order_entries(by_key)
253
+ end
254
+
255
+ # Indexes every pragma by key, resolving the value where both sides declare
256
+ # one. A destination-only pragma is recorded exactly when destination-only
257
+ # nodes would be kept.
258
+ def merge_entry_values
259
+ by_key = {}
260
+ template_entries.each { |entry| by_key[entry.key] ||= entry }
261
+ destination_entries.each do |entry|
262
+ merged = merged_entry(by_key[entry.key], entry)
263
+ by_key[entry.key] = merged unless merged.nil?
264
+ end
265
+ by_key
266
+ end
267
+
268
+ # Resolves one key present on the template side (+existing+) against the
269
+ # destination side (+entry+). Returns nil when the pragma should not be
270
+ # recorded at all.
271
+ def merged_entry(existing, entry)
272
+ return merged_conflicting_entry(existing, entry) if existing
273
+
274
+ keep_destination_only? ? entry : nil
275
+ end
276
+
277
+ # Both sides declare the pragma, so it is always kept and only its value is
278
+ # preference resolved. The template's line anchors the rendered position.
279
+ def merged_conflicting_entry(existing, entry)
280
+ Entry.new(key: entry.key, value: preferred_value(existing.value, entry.value), line: existing.line)
281
+ end
282
+
283
+ # Drops template-declared pragmas whose retention rule says they do not
284
+ # survive, mirroring how template-only nodes are dropped.
285
+ def apply_template_retention(by_key)
286
+ template_entries.each do |entry|
287
+ by_key.delete(entry.key) unless keep_template_declared?(entry.key)
288
+ end
289
+ by_key
290
+ end
291
+
292
+ # Template order first, then destination-only pragmas in destination order.
293
+ def order_entries(by_key)
294
+ template_keys = template_entries.map(&:key).uniq
295
+ ordered = template_keys.filter_map { |key| by_key[key] }
296
+ destination_only = destination_entries.map(&:key).uniq - template_keys
297
+ ordered + destination_only.filter_map { |key| by_key[key] }
298
+ end
299
+
300
+ def keep_template_declared?(key)
301
+ template_declares = template_entries.any? { |entry| entry.key == key }
302
+ destination_declares = destination_entries.any? { |entry| entry.key == key }
303
+ # Declared by both: retention is unconditional, only the value is resolved.
304
+ return true if template_declares && destination_declares
305
+ return false unless template_declares
306
+
307
+ # Template-only: retained exactly when template-only NODES are retained.
308
+ # `preference` does not govern this. Measured against ordinary code, a
309
+ # template-only method or constant is dropped under preference: :template
310
+ # when add_template_only_nodes is false, and kept under
311
+ # preference: :destination when it is true - so presence of a one-sided
312
+ # node follows the add/remove flags, while preference only resolves
313
+ # conflicts between nodes both sides declare. Treating preference as an
314
+ # addition trigger would make pragmas resolve differently from code.
315
+ @add_template_only_nodes
316
+ end
317
+
318
+ def keep_destination_only?
319
+ !@remove_template_missing_nodes
320
+ end
321
+
322
+ def preferred_value(template_value, destination_value)
323
+ preference == :template ? template_value : destination_value
324
+ end
325
+
326
+ # Drops leading blank lines so the pragma block is followed by exactly one
327
+ # separator blank line rather than accumulating them across repeated merges
328
+ # (idempotency).
329
+ def drop_leading_blank_lines(lines)
330
+ index = 0
331
+ index += 1 while index < lines.length && lines[index].to_s.strip.empty?
332
+ lines[index..] || []
333
+ end
334
+
335
+ def entries_for(source)
336
+ return [] if source.empty?
337
+
338
+ parse = Prism.parse(source)
339
+ parse.magic_comments.filter_map do |magic_comment|
340
+ key = magic_comment.key.to_s
341
+ next unless PRAGMA_KEYS.include?(key)
342
+
343
+ Entry.new(key: key, value: magic_comment.value.to_s, line: magic_comment.value_loc.start_line)
344
+ end
345
+ end
346
+ end
347
+ # rubocop:enable Metrics/ClassLength
348
+ end
349
+ end
@@ -62,6 +62,10 @@ module Prism
62
62
  template_comments = actual_template.location.respond_to?(:leading_comments) ? actual_template.location.leading_comments : []
63
63
  dest_comments = actual_dest.location.respond_to?(:leading_comments) ? actual_dest.location.leading_comments : []
64
64
  dest_prefix_comment_lines = merger.instance_variable_get(:@dest_prefix_comment_lines)
65
+ # CLASSIFICATION ONLY: filters header comments already claimed as prefix
66
+ # lines so they are not re-emitted inside a recursive body merge. This is
67
+ # comment dedup, not pragma resolution - see Prism::Merge::PragmaMerger,
68
+ # applied by Prism::Merge::SmartMerger#apply_pragma_header.
65
69
  template_prefix_line_numbers = Prism::Merge::MagicCommentSupport.prefix_comment_line_numbers_for_comments(template_comments)
66
70
  dest_claimed = merger.dest_analysis.respond_to?(:claimed_lines) ? merger.dest_analysis.claimed_lines : Set.new
67
71
  template_claimed = merger.template_analysis.respond_to?(:claimed_lines) ? merger.template_analysis.claimed_lines : Set.new
@@ -163,6 +163,12 @@ module Prism
163
163
  Ruby::Merge::BlockDirectiveDetector.directive_content?(content)
164
164
  end
165
165
 
166
+ # CLASSIFICATION ONLY: decides whether a header comment is documentation or
167
+ # a directive/pragma, so the doc surface excludes non-prose comments. This is
168
+ # a documentation concern, not pragma resolution - it does not decide which
169
+ # pragmas survive a merge or where they are placed. See
170
+ # Prism::Merge::PragmaMerger, applied by
171
+ # Prism::Merge::SmartMerger#apply_pragma_header.
166
172
  def magic_comment_line?(entry, content)
167
173
  node = entry[:node]
168
174
  return true if node.respond_to?(:magic_comment_type) && !node.magic_comment_type.nil?
@@ -274,11 +274,57 @@ module Prism
274
274
  # @return [MergeResult] The merge result
275
275
  def perform_merge
276
276
  validate_files!
277
- top_level_merge_runner.merge
277
+ apply_pragma_header(top_level_merge_runner.merge)
278
278
  end
279
279
 
280
280
  private
281
281
 
282
+ # Resolves file-level pragmas as a discrete document and pins the result to
283
+ # the top of the merged output.
284
+ #
285
+ # The main resolver treats a magic comment as an ordinary comment attached to
286
+ # a statement, which satisfies neither axis of pragma semantics: it inherits
287
+ # the host statement's placement, so it can be stranded below code where Ruby
288
+ # ignores it; and it bypasses the add/remove flags that govern statements, so
289
+ # retention follows `preference` alone. PragmaMerger resolves both.
290
+ #
291
+ # Applied after the merge rather than by stripping the inputs, so line numbers
292
+ # recorded in MergeResult#line_metadata keep matching the source files they
293
+ # came from. This mirrors how regions rewrite content post-hoc via
294
+ # update_result_content.
295
+ #
296
+ # @param result [MergeResult]
297
+ # @return [MergeResult]
298
+ def apply_pragma_header(result)
299
+ return result if result.nil?
300
+
301
+ merged_content = result.to_s
302
+ merger = build_pragma_merger
303
+ # Strips pragmas the main resolver emitted anywhere in the file, then pins
304
+ # the preference-resolved block above the first statement (and below any
305
+ # shebang). A no-op when neither side declares a pragma.
306
+ pinned = merger.place_in(PragmaMerger.strip_pragma_lines(merged_content))
307
+ return result if pinned == merged_content
308
+
309
+ # MergeResult#to_s appends a trailing newline, while the inherited
310
+ # content= splits with -1 and so keeps a trailing empty line. Passing the
311
+ # pinned text through unchanged would therefore add a blank line at the end
312
+ # of the file on every merge. Removing exactly one trailing newline makes
313
+ # the assignment round-trip.
314
+ update_result_content(result, pinned.sub(/\n\z/, ''))
315
+ result
316
+ end
317
+
318
+ def build_pragma_merger
319
+ PragmaMerger.new(
320
+ template_source: template_content,
321
+ destination_source: dest_content,
322
+ preference: preference,
323
+ add_template_only_nodes: add_template_only_nodes,
324
+ remove_template_missing_nodes: remove_template_missing_nodes
325
+ )
326
+ end
327
+
282
328
  def record_runtime_session(session)
283
329
  @runtime_session = session
284
330
  end
@@ -5,7 +5,7 @@ module Prism
5
5
  # Version namespace for this gem.
6
6
  module Version
7
7
  # Current gem version.
8
- VERSION = '7.1.9'
8
+ VERSION = '7.1.10'
9
9
  end
10
10
  # Current gem version exposed at the traditional constant location.
11
11
  VERSION = Version::VERSION # Traditional Constant Location
@@ -2,6 +2,18 @@
2
2
 
3
3
  module Prism
4
4
  module Merge
5
+ # Selects and emits the leading/trailing comments attached to a node.
6
+ #
7
+ # == PRAGMAS ARE NOT PLACED HERE
8
+ #
9
+ # prefix_comment_line_numbers_for_comments is consulted to avoid re-emitting
10
+ # header comments already claimed as prefix lines. That is comment dedup within
11
+ # one side's header, NOT pragma resolution: it does not decide which pragmas
12
+ # survive a merge, nor where they are placed.
13
+ #
14
+ # File-level pragmas are resolved by {Prism::Merge::PragmaMerger} and pinned by
15
+ # {Prism::Merge::SmartMerger#apply_pragma_header} after the merge completes.
16
+ # Do not add pragma retention or placement logic to this class.
5
17
  class WrapperCommentSupport
6
18
  include Prism::Merge::SourceLineLookup
7
19
 
data/lib/prism/merge.rb CHANGED
@@ -58,6 +58,7 @@ module Prism
58
58
  autoload :MethodMatchRefiner, 'prism/merge/method_match_refiner'
59
59
  autoload :NestedStatementWalker, 'prism/merge/nested_statement_walker'
60
60
  autoload :NocovNode, 'prism/merge/nocov_node'
61
+ autoload :PragmaMerger, 'prism/merge/pragma_merger'
61
62
  autoload :NoCovWrapper, 'prism/merge/nocov_wrapper'
62
63
  autoload :NodeBodyLayout, 'prism/merge/node_body_layout'
63
64
  autoload :NodeEmissionSupport, 'prism/merge/node_emission_support'
@@ -1235,6 +1236,17 @@ module Prism
1235
1236
  end
1236
1237
  # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
1237
1238
 
1239
+ # Classifies a comment for node-signature and doc-surface purposes.
1240
+ #
1241
+ # == CLASSIFICATION ONLY, NOT PRAGMA RESOLUTION
1242
+ #
1243
+ # The 'magic_comment' kind here labels a comment so it can be matched and
1244
+ # documented. It does not decide whether a pragma survives a merge or where it
1245
+ # is placed - that is Prism::Merge::PragmaMerger, wired in from
1246
+ # Prism::Merge::SmartMerger#apply_pragma_header, which reads Prism's native
1247
+ # ParseResult#magic_comments directly and does not consult this method.
1248
+ #
1249
+ # Do not build pragma retention or placement on this classification.
1238
1250
  def prism_comment_directive(comment_text, magic_comment)
1239
1251
  if comment_text.strip.match?(/\A#\s*smorg:freeze\b/)
1240
1252
  {
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: prism-merge
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.1.9
4
+ version: 7.1.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -43,14 +43,14 @@ dependencies:
43
43
  requirements:
44
44
  - - '='
45
45
  - !ruby/object:Gem::Version
46
- version: 7.1.9
46
+ version: 7.1.10
47
47
  type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - '='
52
52
  - !ruby/object:Gem::Version
53
- version: 7.1.9
53
+ version: 7.1.10
54
54
  - !ruby/object:Gem::Dependency
55
55
  name: prism
56
56
  requirement: !ruby/object:Gem::Requirement
@@ -71,28 +71,28 @@ dependencies:
71
71
  requirements:
72
72
  - - '='
73
73
  - !ruby/object:Gem::Version
74
- version: 7.1.9
74
+ version: 7.1.10
75
75
  type: :runtime
76
76
  prerelease: false
77
77
  version_requirements: !ruby/object:Gem::Requirement
78
78
  requirements:
79
79
  - - '='
80
80
  - !ruby/object:Gem::Version
81
- version: 7.1.9
81
+ version: 7.1.10
82
82
  - !ruby/object:Gem::Dependency
83
83
  name: tree_haver
84
84
  requirement: !ruby/object:Gem::Requirement
85
85
  requirements:
86
86
  - - '='
87
87
  - !ruby/object:Gem::Version
88
- version: 7.1.9
88
+ version: 7.1.10
89
89
  type: :runtime
90
90
  prerelease: false
91
91
  version_requirements: !ruby/object:Gem::Requirement
92
92
  requirements:
93
93
  - - '='
94
94
  - !ruby/object:Gem::Version
95
- version: 7.1.9
95
+ version: 7.1.10
96
96
  - !ruby/object:Gem::Dependency
97
97
  name: version_gem
98
98
  requirement: !ruby/object:Gem::Requirement
@@ -119,20 +119,20 @@ dependencies:
119
119
  requirements:
120
120
  - - "~>"
121
121
  - !ruby/object:Gem::Version
122
- version: '3.0'
122
+ version: '3.1'
123
123
  - - ">="
124
124
  - !ruby/object:Gem::Version
125
- version: 3.0.34
125
+ version: 3.1.8
126
126
  type: :development
127
127
  prerelease: false
128
128
  version_requirements: !ruby/object:Gem::Requirement
129
129
  requirements:
130
130
  - - "~>"
131
131
  - !ruby/object:Gem::Version
132
- version: '3.0'
132
+ version: '3.1'
133
133
  - - ">="
134
134
  - !ruby/object:Gem::Version
135
- version: 3.0.34
135
+ version: 3.1.8
136
136
  - !ruby/object:Gem::Dependency
137
137
  name: bundler-audit
138
138
  requirement: !ruby/object:Gem::Requirement
@@ -210,7 +210,7 @@ dependencies:
210
210
  version: '3.2'
211
211
  - - ">="
212
212
  - !ruby/object:Gem::Version
213
- version: 3.2.3
213
+ version: 3.2.5
214
214
  type: :development
215
215
  prerelease: false
216
216
  version_requirements: !ruby/object:Gem::Requirement
@@ -220,7 +220,7 @@ dependencies:
220
220
  version: '3.2'
221
221
  - - ">="
222
222
  - !ruby/object:Gem::Version
223
- version: 3.2.3
223
+ version: 3.2.5
224
224
  - !ruby/object:Gem::Dependency
225
225
  name: kettle-test
226
226
  requirement: !ruby/object:Gem::Requirement
@@ -230,7 +230,7 @@ dependencies:
230
230
  version: '2.0'
231
231
  - - ">="
232
232
  - !ruby/object:Gem::Version
233
- version: 2.0.21
233
+ version: 2.0.23
234
234
  type: :development
235
235
  prerelease: false
236
236
  version_requirements: !ruby/object:Gem::Requirement
@@ -240,7 +240,7 @@ dependencies:
240
240
  version: '2.0'
241
241
  - - ">="
242
242
  - !ruby/object:Gem::Version
243
- version: 2.0.21
243
+ version: 2.0.23
244
244
  - !ruby/object:Gem::Dependency
245
245
  name: turbo_tests2
246
246
  requirement: !ruby/object:Gem::Requirement
@@ -250,7 +250,7 @@ dependencies:
250
250
  version: '3.2'
251
251
  - - ">="
252
252
  - !ruby/object:Gem::Version
253
- version: 3.2.7
253
+ version: 3.2.14
254
254
  type: :development
255
255
  prerelease: false
256
256
  version_requirements: !ruby/object:Gem::Requirement
@@ -260,7 +260,7 @@ dependencies:
260
260
  version: '3.2'
261
261
  - - ">="
262
262
  - !ruby/object:Gem::Version
263
- version: 3.2.7
263
+ version: 3.2.14
264
264
  - !ruby/object:Gem::Dependency
265
265
  name: ruby-progressbar
266
266
  requirement: !ruby/object:Gem::Requirement
@@ -284,7 +284,7 @@ dependencies:
284
284
  version: '1.0'
285
285
  - - ">="
286
286
  - !ruby/object:Gem::Version
287
- version: 1.0.8
287
+ version: 1.0.9
288
288
  type: :development
289
289
  prerelease: false
290
290
  version_requirements: !ruby/object:Gem::Requirement
@@ -294,7 +294,7 @@ dependencies:
294
294
  version: '1.0'
295
295
  - - ">="
296
296
  - !ruby/object:Gem::Version
297
- version: 1.0.8
297
+ version: 1.0.9
298
298
  - !ruby/object:Gem::Dependency
299
299
  name: gitmoji-regex
300
300
  requirement: !ruby/object:Gem::Requirement
@@ -304,7 +304,7 @@ dependencies:
304
304
  version: '2.0'
305
305
  - - ">="
306
306
  - !ruby/object:Gem::Version
307
- version: 2.0.12
307
+ version: 2.0.15
308
308
  type: :development
309
309
  prerelease: false
310
310
  version_requirements: !ruby/object:Gem::Requirement
@@ -314,7 +314,7 @@ dependencies:
314
314
  version: '2.0'
315
315
  - - ">="
316
316
  - !ruby/object:Gem::Version
317
- version: 2.0.12
317
+ version: 2.0.15
318
318
  description: "☯️ Prism-backed Ruby provider gem for the Structured Merge Ruby family."
319
319
  email:
320
320
  - floss@galtzo.com
@@ -358,6 +358,7 @@ files:
358
358
  - lib/prism/merge/node_wrapper.rb
359
359
  - lib/prism/merge/partial_template_merger.rb
360
360
  - lib/prism/merge/partial_template_node.rb
361
+ - lib/prism/merge/pragma_merger.rb
361
362
  - lib/prism/merge/provider.rb
362
363
  - lib/prism/merge/recursive_merge_policy.rb
363
364
  - lib/prism/merge/recursive_node_body_merger.rb
@@ -376,10 +377,10 @@ licenses:
376
377
  - PolyForm-Small-Business-1.0.0
377
378
  metadata:
378
379
  homepage_uri: https://structuredmerge.org
379
- source_code_uri: https://github.com/structuredmerge/structuredmerge-ruby/tree/v7.1.9
380
- changelog_uri: https://github.com/structuredmerge/structuredmerge-ruby/blob/v7.1.9/CHANGELOG.md
380
+ source_code_uri: https://github.com/structuredmerge/structuredmerge-ruby/tree/v7.1.10
381
+ changelog_uri: https://github.com/structuredmerge/structuredmerge-ruby/blob/v7.1.10/CHANGELOG.md
381
382
  bug_tracker_uri: https://github.com/structuredmerge/structuredmerge-ruby/issues
382
- documentation_uri: https://www.rubydoc.info/gems/prism-merge/7.1.9
383
+ documentation_uri: https://www.rubydoc.info/gems/prism-merge/7.1.10
383
384
  funding_uri: https://github.com/sponsors/pboling
384
385
  wiki_uri: https://github.com/structuredmerge/structuredmerge-ruby/wiki
385
386
  news_uri: https://www.railsbling.com/tags/prism-merge
@@ -409,7 +410,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
409
410
  - !ruby/object:Gem::Version
410
411
  version: '0'
411
412
  requirements: []
412
- rubygems_version: 4.0.17
413
+ rubygems_version: 4.0.20
413
414
  specification_version: 4
414
415
  summary: "☯️ Structured Merge Prism-backed Ruby analysis for Ruby"
415
416
  test_files: []
metadata.gz.sig CHANGED
Binary file