prism-merge 1.1.1 → 1.1.2

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: ae8f1b1cf1ae576917fae80dea44c38c181296c46f60f4dc70546d12ac0e6a90
4
- data.tar.gz: 131295bf35a6da2038ea889f46ee9520f4a83a7daf9d983c2d0b91fb67644985
3
+ metadata.gz: a67c080bc59cb7303a0f2d3da1c3e5518cf57f20bc1e5b0a43e8e003db155209
4
+ data.tar.gz: 9ddc4c529df3107414dc7c325f230d9224a30d3b828053a4ef76f9c38d05ce1b
5
5
  SHA512:
6
- metadata.gz: 82dcffd3874394148696583ee3cddbb6cde3e33f4136ccd43dbae01213e8252999216320192be3af51fc3fd1662a4d7a524a44722254a19513de84d57efbf048
7
- data.tar.gz: 49ab9213cb3ac2e2d691d3460983d18502c7ceef28a594b1c93578c8c3a8fc44c583472f4920aa9482058cd3815fed2365b75951cdf7e4c52b6c1d37150c3ec9
6
+ metadata.gz: 6a88190811477660b57af41696a567ee899a76f04b918aa98d8e4c0ea45d5b0dca9b3eceb2abb8564dbb3912abdc35bf89b4a3736000b17d583d869e2f688fbd
7
+ data.tar.gz: 4cd9b298503649db46812dda1cfd2a04f37c0db776ff5dcc684ebe199fd81f6e469c13ed27ac375c7d23c6ae9e14a00afa2db5c8f8698a94ed080db595d8526d
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -30,6 +30,23 @@ Please file a bug if you notice a violation of semantic versioning.
30
30
 
31
31
  ### Security
32
32
 
33
+ ## [1.1.2] - 2025-12-04
34
+
35
+ - TAG: [v1.1.2][1.1.2t]
36
+ - COVERAGE: 96.66% -- 868/898 lines in 9 files
37
+ - BRANCH COVERAGE: 82.84% -- 338/408 branches in 9 files
38
+ - 100.00% documented
39
+
40
+ ### Added
41
+
42
+ - `body_has_mergeable_statements?` private method to check if a block body contains statements that can be signature-matched
43
+ - `mergeable_statement?` private method to determine if a node type can generate signatures for merging
44
+ - `max_recursion_depth` option (defaults to `Float::INFINITY`) as a safety valve for edge cases
45
+
46
+ ### Fixed
47
+
48
+ - **Fixed infinite recursion** when merging `CallNode` blocks (like `git_source`) that have matching signatures but non-mergeable body content (e.g., just string literals). The fix detects when a block body contains only literals/expressions with no signature-matchable statements and treats the node atomically instead of recursing.
49
+
33
50
  ## [1.1.1] - 2025-12-04
34
51
 
35
52
  - TAG: [v1.1.1][1.1.1t]
@@ -154,7 +171,9 @@ Please file a bug if you notice a violation of semantic versioning.
154
171
 
155
172
  - Initial release
156
173
 
157
- [Unreleased]: https://github.com/kettle-rb/prism-merge/compare/v1.1.1...HEAD
174
+ [Unreleased]: https://github.com/kettle-rb/prism-merge/compare/v1.1.2...HEAD
175
+ [1.1.2]: https://github.com/kettle-rb/prism-merge/compare/v1.1.1...v1.1.2
176
+ [1.1.2t]: https://github.com/kettle-rb/prism-merge/releases/tag/v1.1.2
158
177
  [1.1.1]: https://github.com/kettle-rb/prism-merge/compare/v1.1.0...v1.1.1
159
178
  [1.1.1t]: https://github.com/kettle-rb/prism-merge/releases/tag/v1.1.1
160
179
  [1.1.0]: https://github.com/kettle-rb/prism-merge/compare/v1.0.3...v1.1.0
data/README.md CHANGED
@@ -298,6 +298,33 @@ merger = Prism::Merge::SmartMerger.new(
298
298
  # Result: Existing configs keep destination values, new configs added from template
299
299
  ```
300
300
 
301
+ ### Recursion Depth Limit
302
+
303
+ Prism::Merge automatically detects when block bodies contain only literals or simple expressions (no mergeable statements) and treats them atomically. However, as a safety valve for edge cases, you can limit recursion depth:
304
+
305
+ ```ruby
306
+ # Limit recursive merging to 3 levels deep
307
+ merger = Prism::Merge::SmartMerger.new(
308
+ template,
309
+ destination,
310
+ max_recursion_depth: 3,
311
+ )
312
+
313
+ # Disable recursive merging entirely (treat all nodes atomically)
314
+ merger = Prism::Merge::SmartMerger.new(
315
+ template,
316
+ destination,
317
+ max_recursion_depth: 0,
318
+ )
319
+ ```
320
+
321
+ **When to use:**
322
+
323
+ - **`Float::INFINITY`** (default) - Normal operation, recursion terminates naturally based on content analysis.
324
+ - NOTE: If you get `stack level too deep (SystemStackError)`, please file a [bug](https://github.com/kettle-rb/prism-merge/issues)!
325
+ - **Finite value** - Safety valve if you encounter edge cases with unexpected deep recursion
326
+ - **`0`** - Disable recursive merging entirely; all matching nodes are treated atomically
327
+
301
328
  ### Custom Signature Generator
302
329
 
303
330
  By default, Prism::Merge uses intelligent structural signatures to match nodes. The signature determines how nodes are matched between template and destination files.
@@ -332,7 +359,7 @@ The following node types support **recursive body merging**, where nested conten
332
359
  - `ClassNode` - class bodies are recursively merged
333
360
  - `ModuleNode` - module bodies are recursively merged
334
361
  - `SingletonClassNode` - singleton class bodies are recursively merged
335
- - `CallNode` with block - block bodies are recursively merged (e.g., `configure do ... end`)
362
+ - `CallNode` with block - block bodies are recursively merged **only when the body contains mergeable statements** (e.g., `describe do ... end` with nested `it` blocks). Blocks containing only literals or simple expressions (like `git_source(:github) { |repo| "https://..." }`) are treated atomically.
336
363
  - `BeginNode` - begin/rescue/ensure blocks are recursively merged
337
364
 
338
365
  #### Custom Signature Generator
@@ -1107,7 +1134,7 @@ Thanks for RTFM. ☺️
1107
1134
  [📌gitmoji]: https://gitmoji.dev
1108
1135
  [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
1109
1136
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
1110
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-0.887-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
1137
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-0.898-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
1111
1138
  [🔐security]: SECURITY.md
1112
1139
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
1113
1140
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
@@ -93,6 +93,12 @@ module Prism
93
93
  # Default: "prism-merge" (looks for # prism-merge:freeze / # prism-merge:unfreeze)
94
94
  # Freeze blocks preserve destination content unchanged during merge.
95
95
  #
96
+ # @param max_recursion_depth [Integer, Float] Maximum depth for recursive body merging.
97
+ # Default: Float::INFINITY (no limit). This is a safety valve that users can set
98
+ # if they encounter edge cases. Normal merging terminates naturally based on
99
+ # body content analysis (blocks with non-mergeable content like literals are
100
+ # not recursed into).
101
+ #
96
102
  # @raise [TemplateParseError] If template has syntax errors
97
103
  # @raise [DestinationParseError] If destination has syntax errors
98
104
  #
@@ -131,12 +137,14 @@ module Prism
131
137
  # destination,
132
138
  # signature_generator: sig_gen
133
139
  # )
134
- def initialize(template_content, dest_content, signature_generator: nil, signature_match_preference: :destination, add_template_only_nodes: false, freeze_token: FileAnalysis::DEFAULT_FREEZE_TOKEN)
140
+ def initialize(template_content, dest_content, signature_generator: nil, signature_match_preference: :destination, add_template_only_nodes: false, freeze_token: FileAnalysis::DEFAULT_FREEZE_TOKEN, max_recursion_depth: Float::INFINITY, current_depth: 0)
135
141
  @template_content = template_content
136
142
  @dest_content = dest_content
137
143
  @signature_match_preference = signature_match_preference
138
144
  @add_template_only_nodes = add_template_only_nodes
139
145
  @freeze_token = freeze_token
146
+ @max_recursion_depth = max_recursion_depth
147
+ @current_depth = current_depth
140
148
  @template_analysis = FileAnalysis.new(template_content, signature_generator: signature_generator, freeze_token: freeze_token)
141
149
  @dest_analysis = FileAnalysis.new(dest_content, signature_generator: signature_generator, freeze_token: freeze_token)
142
150
  @aligner = FileAligner.new(@template_analysis, @dest_analysis)
@@ -415,9 +423,14 @@ module Prism
415
423
  # - Conditional nodes (if/unless) - treated as atomic units
416
424
  # - Classes/modules/blocks containing freeze blocks - frozen content would be lost
417
425
  # - Nodes of different types
426
+ # - Blocks whose body contains only literals/expressions with no mergeable statements
427
+ # - When max_recursion_depth has been reached (safety valve)
418
428
  def should_merge_recursively?(template_node, dest_node)
419
429
  return false unless template_node && dest_node
420
430
 
431
+ # Safety valve: stop recursion if max depth reached
432
+ return false if @current_depth >= @max_recursion_depth
433
+
421
434
  # Both nodes must be the same type
422
435
  return false unless template_node.class == dest_node.class
423
436
 
@@ -427,8 +440,10 @@ module Prism
427
440
  # Class/module definitions - merge their body contents
428
441
  true
429
442
  when Prism::CallNode
430
- # Only merge if both have blocks
431
- template_node.block && dest_node.block
443
+ # Only merge if both have blocks with mergeable content
444
+ template_node.block && dest_node.block &&
445
+ body_has_mergeable_statements?(template_node.block.body) &&
446
+ body_has_mergeable_statements?(dest_node.block.body)
432
447
  when Prism::BeginNode
433
448
  # begin/rescue/ensure blocks - merge statements
434
449
  template_node.statements && dest_node.statements
@@ -456,6 +471,45 @@ module Prism
456
471
  true
457
472
  end
458
473
 
474
+ # Check if a body (StatementsNode) contains statements that could be merged.
475
+ #
476
+ # Mergeable statements are those that can generate signatures and be
477
+ # independently matched between template and destination. This includes
478
+ # method definitions, class/module definitions, method calls, assignments, etc.
479
+ #
480
+ # Bodies containing only literals (strings, numbers, arrays, hashes) or
481
+ # simple expressions should not be recursively merged as there's nothing
482
+ # to align - they should be treated atomically.
483
+ #
484
+ # @param body [Prism::StatementsNode, nil] The body to check
485
+ # @return [Boolean] true if the body contains mergeable statements
486
+ # @api private
487
+ def body_has_mergeable_statements?(body)
488
+ return false unless body.is_a?(Prism::StatementsNode)
489
+ return false if body.body.empty?
490
+
491
+ body.body.any? { |stmt| mergeable_statement?(stmt) }
492
+ end
493
+
494
+ # Check if a statement is mergeable (can generate a signature).
495
+ #
496
+ # @param node [Prism::Node] The node to check
497
+ # @return [Boolean] true if this node type can be merged
498
+ # @api private
499
+ def mergeable_statement?(node)
500
+ case node
501
+ when Prism::CallNode, Prism::DefNode, Prism::ClassNode, Prism::ModuleNode,
502
+ Prism::SingletonClassNode, Prism::ConstantWriteNode, Prism::ConstantPathWriteNode,
503
+ Prism::LocalVariableWriteNode, Prism::InstanceVariableWriteNode,
504
+ Prism::ClassVariableWriteNode, Prism::GlobalVariableWriteNode,
505
+ Prism::MultiWriteNode, Prism::IfNode, Prism::UnlessNode, Prism::CaseNode,
506
+ Prism::BeginNode
507
+ true
508
+ else
509
+ false
510
+ end
511
+ end
512
+
459
513
  # Check if a node's body contains freeze block markers.
460
514
  #
461
515
  # @param node [Prism::Node] The node to check
@@ -512,6 +566,7 @@ module Prism
512
566
  # @note The nested merger is configured with:
513
567
  # - Same signature_generator, signature_match_preference, and add_template_only_nodes
514
568
  # - freeze_token: nil (freeze blocks not processed in nested context)
569
+ # - Incremented current_depth to track recursion level
515
570
  #
516
571
  # @api private
517
572
  def merge_node_body_recursively(template_node, dest_node, anchor)
@@ -519,7 +574,7 @@ module Prism
519
574
  template_body = extract_node_body(template_node, @template_analysis)
520
575
  dest_body = extract_node_body(dest_node, @dest_analysis)
521
576
 
522
- # Recursively merge the bodies
577
+ # Recursively merge the bodies with incremented depth
523
578
  body_merger = SmartMerger.new(
524
579
  template_body,
525
580
  dest_body,
@@ -527,6 +582,8 @@ module Prism
527
582
  signature_match_preference: @signature_match_preference,
528
583
  add_template_only_nodes: @add_template_only_nodes,
529
584
  freeze_token: nil, # Don't process freeze blocks in nested context
585
+ max_recursion_depth: @max_recursion_depth,
586
+ current_depth: @current_depth + 1,
530
587
  )
531
588
  merged_body = body_merger.merge.rstrip
532
589
 
@@ -5,7 +5,7 @@ module Prism
5
5
  # Version information for Prism::Merge
6
6
  module Version
7
7
  # Current version of the prism-merge gem
8
- VERSION = "1.1.1"
8
+ VERSION = "1.1.2"
9
9
  end
10
10
  VERSION = Version::VERSION # traditional location
11
11
  end
data/sig/prism/merge.rbs CHANGED
@@ -323,7 +323,10 @@ module Prism
323
323
  String dest_content,
324
324
  ?signature_generator: (^(untyped) -> Array[untyped])?,
325
325
  ?signature_match_preference: Symbol,
326
- ?add_template_only_nodes: bool
326
+ ?add_template_only_nodes: bool,
327
+ ?freeze_token: String?,
328
+ ?max_recursion_depth: (Integer | Float),
329
+ ?current_depth: Integer
327
330
  ) -> void
328
331
 
329
332
  def merge: () -> String
@@ -353,9 +356,19 @@ module Prism
353
356
  def find_node_at_line: (FileAnalysis analysis, Integer line_num) -> untyped?
354
357
 
355
358
  # Determine if two matching nodes should be recursively merged
356
- # Returns true for ClassNode, ModuleNode, SingletonClassNode, CallNode with blocks, BeginNode
359
+ # Returns true for ClassNode, ModuleNode, SingletonClassNode, and CallNode/BeginNode
360
+ # with blocks that contain mergeable statements.
361
+ # Returns false if max_recursion_depth has been reached (safety valve).
357
362
  def should_merge_recursively?: (untyped? template_node, untyped? dest_node) -> bool
358
363
 
364
+ # Check if a body (StatementsNode) contains statements that could be merged
365
+ # Returns true if body contains CallNode, DefNode, ClassNode, assignments, etc.
366
+ def body_has_mergeable_statements?: (untyped? body) -> bool
367
+
368
+ # Check if a statement is mergeable (can generate a signature)
369
+ # Returns true for CallNode, DefNode, ClassNode, ModuleNode, assignments, conditionals, etc.
370
+ def mergeable_statement?: (untyped node) -> bool
371
+
359
372
  # Check if a node's body contains freeze block markers
360
373
  def node_contains_freeze_blocks?: (untyped node) -> bool
361
374
 
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: 1.1.1
4
+ version: 1.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -265,10 +265,10 @@ licenses:
265
265
  - MIT
266
266
  metadata:
267
267
  homepage_uri: https://prism-merge.galtzo.com/
268
- source_code_uri: https://github.com/kettle-rb/prism-merge/tree/v1.1.1
269
- changelog_uri: https://github.com/kettle-rb/prism-merge/blob/v1.1.1/CHANGELOG.md
268
+ source_code_uri: https://github.com/kettle-rb/prism-merge/tree/v1.1.2
269
+ changelog_uri: https://github.com/kettle-rb/prism-merge/blob/v1.1.2/CHANGELOG.md
270
270
  bug_tracker_uri: https://github.com/kettle-rb/prism-merge/issues
271
- documentation_uri: https://www.rubydoc.info/gems/prism-merge/1.1.1
271
+ documentation_uri: https://www.rubydoc.info/gems/prism-merge/1.1.2
272
272
  funding_uri: https://github.com/sponsors/pboling
273
273
  wiki_uri: https://github.com/kettle-rb/prism-merge/wiki
274
274
  news_uri: https://www.railsbling.com/tags/prism-merge
metadata.gz.sig CHANGED
Binary file