Skip to content

Stave sharing - Part 3#33838

Open
mike-spa wants to merge 4 commits into
musescore:mainfrom
mike-spa:STAVE_SHARING
Open

Stave sharing - Part 3#33838
mike-spa wants to merge 4 commits into
musescore:mainfrom
mike-spa:STAVE_SHARING

Conversation

@mike-spa

Copy link
Copy Markdown
Contributor

No description provided.

mike-spa added 3 commits June 15, 2026 17:45
Dynamics, text, etc
Because the current m_sharedItem may have been already invalidated, so don't try to use it.
@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

TEXTBASE_TYPES is introduced as a static unordered_set<ElementType> in types.h, and isTextBase() is rewritten to use muse::contains against it instead of an explicit chain of comparisons. connectSharedItem removes its fallback disconnect branch, replacing it with an assertion that origin/shared state is clean before proceeding. StaveSharingLayout receives three new private helpers—checkAnnotationsForSameVoice, checkNoteSpannersForUnison, and makeSharedTiesAndNoteSpanners—declared in the header and implemented in the source. Unison validation in isUnison and canGoToSameVoice is strengthened with Note::isExactUnison, spanner/tie compatibility checks, and annotation equivalence. disconnectAll is rewritten to comprehensively disconnect annotations, tuplet chains, chord notes, ties, and spanners. makeSharedChordRests gains nested tuplet construction and switches note matching to isExactUnison. makeSharedAnnotations adds XML-text equality validation and voice-based placement logic. cleanup is extended to prune orphaned annotations, tuplets, ties, and spanners with endpoint-aware conditions.

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (2 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Description check ⚠️ Warning No pull request description was provided, leaving all required template sections unfilled including issue resolution, motivation, CLA signature, and commit message validation. Add a comprehensive pull request description following the repository template, including the GitHub issue number, motivation for changes, completed checklist items, and commit message details.
Docstring Coverage ⚠️ Warning Docstring coverage is 6.25% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Stave sharing - Part 3' is partially related to the changeset but overly vague and generic, lacking specificity about the actual improvements. Consider a more specific title that highlights the main change, such as 'Strengthen unison validation and refactor shared-notation rebuilding in stave sharing' to better convey the significant logic improvements.
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

Review ran into problems

🔥 Problems

Linked repositories: Your configuration references 1 linked repositories, but your current plan allows 0. Analyzed ``, skipped musescore/muse_framework.git.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/engraving/rendering/score/stavesharinglayout.cpp`:
- Around line 411-417: The forward-tie compatibility check is comparing the
wrong endpoint for tie consistency. In the condition that checks
`tieFor1->startElement()` against `tieFor2->startElement()`, you are checking
the start of the ties when you should be checking the end. Replace the
`startElement()` calls with `endElement()` calls (or `endNote()` if that is the
appropriate method) to correctly compare whether forward ties have matching
open-vs-closed endpoint configurations.
- Around line 703-708: There is a null dereference bug in the tieFor cloning
logic. When the condition checks if sharedTieFor is null with if
(!sharedTieFor), the code immediately tries to call sharedTieFor->clone() inside
that block, which dereferences a null pointer and causes a crash. The fix is to
clone tieFor (the original tie object) instead of sharedTieFor when sharedTieFor
is null. Change the clone call from sharedTieFor->clone() to tieFor->clone() to
use the correct source object for cloning.
- Around line 375-395: The annotation matching logic does not enforce one-to-one
correspondence between annotations on the previous and next tracks. Currently,
when iterating through annotationsOnPrevTrack and comparing each against
candidates in annotationsOnNextTrack via equal_range, there is no mechanism to
remove or mark matched annotations as consumed. This means a single annotation
from the next track can be matched multiple times, breaking duplicate handling.
To fix this, modify the matching logic to track which annotations from
annotationsOnNextTrack have already been matched (for example, using a set of
matched keys or erasing matched items), and ensure that each annotation is only
matched once. After finding a matching annotation in the inner loop (the loop
iterating through equal_range results), immediately mark or remove it so
subsequent iterations in the outer loop cannot reuse it. This will enforce
strict one-to-one pairing of annotations between the two tracks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7217ee5e-37fa-4426-a4f4-4c3b1b31a392

📥 Commits

Reviewing files that changed from the base of the PR and between d402227 and bc1de3d.

📒 Files selected for processing (5)
  • src/engraving/dom/engravingitem.cpp
  • src/engraving/dom/engravingobject.cpp
  • src/engraving/rendering/score/stavesharinglayout.cpp
  • src/engraving/rendering/score/stavesharinglayout.h
  • src/engraving/types/types.h
💤 Files with no reviewable changes (1)
  • src/engraving/dom/engravingitem.cpp

Comment on lines +375 to +395
if (annotationsOnPrevTrack.size() != annotationsOnNextTrack.size()) {
return false;
}

for (auto [type, item] : annotationsOnPrevTrack) {
auto range = annotationsOnNextTrack.equal_range(type);
if (range.first == range.second) {
// No annotation of this type exists in nextTrack
return false;
}

for (auto i = range.first; i != range.second; ++i) {
EngravingItem* nextItem = i->second;
if (muse::contains(TEXTBASE_TYPES, type)) {
if (toTextBase(item)->xmlText() != toTextBase(nextItem)->xmlText()) {
return false;
}
}
// TODO: other types will probably need other checks
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Annotation matching is not one-to-one and can both reject valid pairs and accept invalid multiplicities.

At Line 386, each previous annotation is compared against all same-type candidates on the next track, and matches are never consumed. This breaks duplicate handling (especially text annotations) and can misclassify same-voice compatibility.

Suggested fix
 bool StaveSharingLayout::checkAnnotationsForSameVoice(Segment* segment, track_idx_t prevTrack, track_idx_t nextTrack)
 {
-    std::multimap<ElementType, EngravingItem*> annotationsOnPrevTrack;
-    std::multimap<ElementType, EngravingItem*> annotationsOnNextTrack;
+    std::multimap<ElementType, EngravingItem*> annotationsOnPrevTrack;
+    std::multimap<ElementType, EngravingItem*> annotationsOnNextTrack;
@@
-    for (auto [type, item] : annotationsOnPrevTrack) {
-        auto range = annotationsOnNextTrack.equal_range(type);
-        if (range.first == range.second) {
-            // No annotation of this type exists in nextTrack
-            return false;
-        }
-
-        for (auto i = range.first; i != range.second; ++i) {
-            EngravingItem* nextItem = i->second;
-            if (muse::contains(TEXTBASE_TYPES, type)) {
-                if (toTextBase(item)->xmlText() != toTextBase(nextItem)->xmlText()) {
-                    return false;
-                }
-            }
-            // TODO: other types will probably need other checks
-        }
-    }
+    for (auto it = annotationsOnPrevTrack.begin(); it != annotationsOnPrevTrack.end();) {
+        ElementType type = it->first;
+        auto [pBeg, pEnd] = annotationsOnPrevTrack.equal_range(type);
+        auto [nBeg, nEnd] = annotationsOnNextTrack.equal_range(type);
+        if (std::distance(pBeg, pEnd) != std::distance(nBeg, nEnd)) {
+            return false;
+        }
+        if (muse::contains(TEXTBASE_TYPES, type)) {
+            std::multiset<String> prevTexts;
+            std::multiset<String> nextTexts;
+            for (auto p = pBeg; p != pEnd; ++p) {
+                prevTexts.insert(toTextBase(p->second)->xmlText());
+            }
+            for (auto n = nBeg; n != nEnd; ++n) {
+                nextTexts.insert(toTextBase(n->second)->xmlText());
+            }
+            if (prevTexts != nextTexts) {
+                return false;
+            }
+        }
+        it = pEnd;
+    }
 
     return true;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (annotationsOnPrevTrack.size() != annotationsOnNextTrack.size()) {
return false;
}
for (auto [type, item] : annotationsOnPrevTrack) {
auto range = annotationsOnNextTrack.equal_range(type);
if (range.first == range.second) {
// No annotation of this type exists in nextTrack
return false;
}
for (auto i = range.first; i != range.second; ++i) {
EngravingItem* nextItem = i->second;
if (muse::contains(TEXTBASE_TYPES, type)) {
if (toTextBase(item)->xmlText() != toTextBase(nextItem)->xmlText()) {
return false;
}
}
// TODO: other types will probably need other checks
}
}
if (annotationsOnPrevTrack.size() != annotationsOnNextTrack.size()) {
return false;
}
for (auto it = annotationsOnPrevTrack.begin(); it != annotationsOnPrevTrack.end();) {
ElementType type = it->first;
auto [pBeg, pEnd] = annotationsOnPrevTrack.equal_range(type);
auto [nBeg, nEnd] = annotationsOnNextTrack.equal_range(type);
if (std::distance(pBeg, pEnd) != std::distance(nBeg, nEnd)) {
return false;
}
if (muse::contains(TEXTBASE_TYPES, type)) {
std::multiset<String> prevTexts;
std::multiset<String> nextTexts;
for (auto p = pBeg; p != pEnd; ++p) {
prevTexts.insert(toTextBase(p->second)->xmlText());
}
for (auto n = nBeg; n != nEnd; ++n) {
nextTexts.insert(toTextBase(n->second)->xmlText());
}
if (prevTexts != nextTexts) {
return false;
}
}
it = pEnd;
}
return true;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/engraving/rendering/score/stavesharinglayout.cpp` around lines 375 - 395,
The annotation matching logic does not enforce one-to-one correspondence between
annotations on the previous and next tracks. Currently, when iterating through
annotationsOnPrevTrack and comparing each against candidates in
annotationsOnNextTrack via equal_range, there is no mechanism to remove or mark
matched annotations as consumed. This means a single annotation from the next
track can be matched multiple times, breaking duplicate handling. To fix this,
modify the matching logic to track which annotations from annotationsOnNextTrack
have already been matched (for example, using a set of matched keys or erasing
matched items), and ensure that each annotation is only matched once. After
finding a matching annotation in the inner loop (the loop iterating through
equal_range results), immediately mark or remove it so subsequent iterations in
the outer loop cannot reuse it. This will enforce strict one-to-one pairing of
annotations between the two tracks.

Comment thread src/engraving/rendering/score/stavesharinglayout.cpp
Comment on lines +703 to +708
Tie* tieFor = originNote->tieFor();
if (tieFor && !tieFor->endNote()) { // If it does have an end note we add it when we process the end note
Tie* sharedTieFor = sharedNote->tieFor();
if (!sharedTieFor) {
sharedTieFor = toTie(sharedTieFor->clone());
sharedTieFor->setNoteSpan(sharedNote, nullptr);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Null dereference when creating sharedTieFor.

At Line 707, sharedTieFor is null inside the if (!sharedTieFor) block, but is immediately dereferenced for cloning. This is a deterministic crash path.

Suggested fix
         Tie* sharedTieFor = sharedNote->tieFor();
         if (!sharedTieFor) {
-            sharedTieFor = toTie(sharedTieFor->clone());
+            sharedTieFor = toTie(tieFor->clone());
             sharedTieFor->setNoteSpan(sharedNote, nullptr);
 
             score->undoAddElement(sharedTieFor);
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Tie* tieFor = originNote->tieFor();
if (tieFor && !tieFor->endNote()) { // If it does have an end note we add it when we process the end note
Tie* sharedTieFor = sharedNote->tieFor();
if (!sharedTieFor) {
sharedTieFor = toTie(sharedTieFor->clone());
sharedTieFor->setNoteSpan(sharedNote, nullptr);
Tie* tieFor = originNote->tieFor();
if (tieFor && !tieFor->endNote()) { // If it does have an end note we add it when we process the end note
Tie* sharedTieFor = sharedNote->tieFor();
if (!sharedTieFor) {
sharedTieFor = toTie(tieFor->clone());
sharedTieFor->setNoteSpan(sharedNote, nullptr);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/engraving/rendering/score/stavesharinglayout.cpp` around lines 703 - 708,
There is a null dereference bug in the tieFor cloning logic. When the condition
checks if sharedTieFor is null with if (!sharedTieFor), the code immediately
tries to call sharedTieFor->clone() inside that block, which dereferences a null
pointer and causes a crash. The fix is to clone tieFor (the original tie object)
instead of sharedTieFor when sharedTieFor is null. Change the clone call from
sharedTieFor->clone() to tieFor->clone() to use the correct source object for
cloning.

@miiizen miiizen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spotted a few things, but this is looking so good! 😍

EngravingItem* sharedItem = seg->findAnnotation(originItem->type(), sharedTrack, sharedTrack);
if (sharedItem && sharedItem->isTextBase()) {
if (toTextBase(sharedItem)->xmlText() != toTextBase(originItem)->xmlText()) {
sharedItem = nullptr; // Not the one we are looking for

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't look like it handles two pieces of text on the same segment correctly:
We probably need to use Segment::findAnnotations and loop over the result

Image Image Image Image

std::vector<EngravingItem*> annotations = seg->annotations(); // Copy because we may remove elements
for (EngravingItem* item : annotations) {
if (item->track() >= startTrack && item->track() < endTrack) {
if (item->originItems().empty()) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be in the condition above

Comment thread src/engraving/rendering/score/stavesharinglayout.cpp
if (tieFor && !tieFor->endNote()) { // If it does have an end note we add it when we process the end note
Tie* sharedTieFor = sharedNote->tieFor();
if (!sharedTieFor) {
sharedTieFor = toTie(sharedTieFor->clone());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sharedTieFor = toTie(tieFor->clone());

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants