From 06f3644f963d71581bf7838232e356d1133c8217 Mon Sep 17 00:00:00 2001 From: Andy Black Date: Wed, 8 Jul 2026 11:43:18 -0400 Subject: [PATCH 1/6] Fix for LT-22563 Change-Id: I60967f8053f07355f03c3b0a320791d1880d1822 --- Src/LexText/ParserCore/XAmpleParser.cs | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/Src/LexText/ParserCore/XAmpleParser.cs b/Src/LexText/ParserCore/XAmpleParser.cs index df7d2e0150..9f3a3a5c6f 100644 --- a/Src/LexText/ParserCore/XAmpleParser.cs +++ b/Src/LexText/ParserCore/XAmpleParser.cs @@ -262,7 +262,7 @@ private static bool TryCreateParseMorph(LcmCache cache, XElement morphElem, out } // Irregulary inflected forms can have a combination MSA hvo: the LexEntry hvo, a period, and an index to the LexEntryRef - Tuple msaTuple = ProcessMsaHvo(msaHvo); + Tuple msaTuple = ProcessMsaHvo(msaHvo); ICmObject objMsa; if (!cache.ServiceLocator.GetInstance().TryGetObject(msaTuple.Item1, out objMsa)) { @@ -286,10 +286,19 @@ private static bool TryCreateParseMorph(LcmCache cache, XElement morphElem, out ILexEntryRef lexEntryRef = msaAsLexEntry.EntryRefsOS[msaTuple.Item2]; if (lexEntryRef != null && lexEntryRef.ComponentLexemesRS.Count() > 0) { - // make sure there is at least one component lexeme (LT-22328) - ILexSense sense = MorphServices.GetMainOrFirstSenseOfVariant(lexEntryRef); var inflType = lexEntryRef.VariantEntryTypesRS[0] as ILexEntryInflType; - morph = new ParseMorph(form, sense.MorphoSyntaxAnalysisRA, inflType); + IMoStemMsa stemMsa = (IMoStemMsa)cache.ServiceLocator.GetInstance().GetObject(msaTuple.Item3); + if (stemMsa != null) + { + // use the msa itself + morph = new ParseMorph(form, stemMsa, inflType); + } + else + { + // make sure there is at least one component lexeme (LT-22328) + ILexSense sense = MorphServices.GetMainOrFirstSenseOfVariant(lexEntryRef); + morph = new ParseMorph(form, sense.MorphoSyntaxAnalysisRA, inflType); + } return true; } } @@ -300,10 +309,13 @@ private static bool TryCreateParseMorph(LcmCache cache, XElement morphElem, out return true; } - private static Tuple ProcessMsaHvo(string msaHvo) + private static Tuple ProcessMsaHvo(string msaHvo) { string[] msaHvoParts = msaHvo.Split('.'); - return Tuple.Create(int.Parse(msaHvoParts[0]), msaHvoParts.Length == 2 ? int.Parse(msaHvoParts[1]) : 0); + // the msa hvo has one part or three parts separated by a period. + // in the latter case, it is the lex entry hvo, the lex ref hvo, and the msa hvo + return Tuple.Create(int.Parse(msaHvoParts[0]), msaHvoParts.Length == 3 ? int.Parse(msaHvoParts[1]) : 0, + msaHvoParts.Length == 3 ? int.Parse(msaHvoParts[2]) : 0); } public XDocument ParseWordXml(string word) From 9980d4144e70862082756fa6cf07411076007f47 Mon Sep 17 00:00:00 2001 From: Andy Black Date: Wed, 8 Jul 2026 11:43:18 -0400 Subject: [PATCH 2/6] Avoid duplicatre code for ProcessMsaHvo() Change-Id: I2223a0b3b3be5d4831cb2d25c591dcf4f47a0afb --- Src/LexText/ParserCore/ParserXmlWriterExtensions.cs | 2 +- Src/LexText/ParserCore/XAmpleParser.cs | 11 +---------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/Src/LexText/ParserCore/ParserXmlWriterExtensions.cs b/Src/LexText/ParserCore/ParserXmlWriterExtensions.cs index 9172e2ed94..9cd0511f5f 100644 --- a/Src/LexText/ParserCore/ParserXmlWriterExtensions.cs +++ b/Src/LexText/ParserCore/ParserXmlWriterExtensions.cs @@ -18,7 +18,7 @@ namespace SIL.FieldWorks.WordWorks.Parser { internal static class ParserXmlWriterExtensions { - private static Tuple ProcessMsaHvo(string msaHvo) + public static Tuple ProcessMsaHvo(string msaHvo) { string[] msaHvoParts = msaHvo.Split('.'); // the msa hvo has one part or three parts separated by a period. diff --git a/Src/LexText/ParserCore/XAmpleParser.cs b/Src/LexText/ParserCore/XAmpleParser.cs index 9f3a3a5c6f..0171caba5f 100644 --- a/Src/LexText/ParserCore/XAmpleParser.cs +++ b/Src/LexText/ParserCore/XAmpleParser.cs @@ -262,7 +262,7 @@ private static bool TryCreateParseMorph(LcmCache cache, XElement morphElem, out } // Irregulary inflected forms can have a combination MSA hvo: the LexEntry hvo, a period, and an index to the LexEntryRef - Tuple msaTuple = ProcessMsaHvo(msaHvo); + Tuple msaTuple = ParserXmlWriterExtensions.ProcessMsaHvo(msaHvo); ICmObject objMsa; if (!cache.ServiceLocator.GetInstance().TryGetObject(msaTuple.Item1, out objMsa)) { @@ -309,15 +309,6 @@ private static bool TryCreateParseMorph(LcmCache cache, XElement morphElem, out return true; } - private static Tuple ProcessMsaHvo(string msaHvo) - { - string[] msaHvoParts = msaHvo.Split('.'); - // the msa hvo has one part or three parts separated by a period. - // in the latter case, it is the lex entry hvo, the lex ref hvo, and the msa hvo - return Tuple.Create(int.Parse(msaHvoParts[0]), msaHvoParts.Length == 3 ? int.Parse(msaHvoParts[1]) : 0, - msaHvoParts.Length == 3 ? int.Parse(msaHvoParts[2]) : 0); - } - public XDocument ParseWordXml(string word) { CheckDisposed(); From 68a9a905aa5c3e66968c07614acd4219b2c3483a Mon Sep 17 00:00:00 2001 From: Andy Black Date: Wed, 8 Jul 2026 11:43:18 -0400 Subject: [PATCH 3/6] Fix comment in TryCreateParseMorph() Change-Id: Ida91b219515634f14ba83f594d226f68d5b16585 --- Src/LexText/ParserCore/XAmpleParser.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Src/LexText/ParserCore/XAmpleParser.cs b/Src/LexText/ParserCore/XAmpleParser.cs index 0171caba5f..75c5de2f60 100644 --- a/Src/LexText/ParserCore/XAmpleParser.cs +++ b/Src/LexText/ParserCore/XAmpleParser.cs @@ -261,7 +261,8 @@ private static bool TryCreateParseMorph(LcmCache cache, XElement morphElem, out return true; } - // Irregulary inflected forms can have a combination MSA hvo: the LexEntry hvo, a period, and an index to the LexEntryRef + // Irregulary inflected forms can have a combination MSA hvo: the LexEntry hvo, a period, an index to the LexEntryRef, + // another period and the MSA hvo Tuple msaTuple = ParserXmlWriterExtensions.ProcessMsaHvo(msaHvo); ICmObject objMsa; if (!cache.ServiceLocator.GetInstance().TryGetObject(msaTuple.Item1, out objMsa)) From bd03e81ae09376477221193b6d1c21dc05e37118 Mon Sep 17 00:00:00 2001 From: John Lambert Date: Wed, 8 Jul 2026 11:43:18 -0400 Subject: [PATCH 4/6] Add regression test for LT-22563 MSI DbRef parsing TryCreateParseMorph must use the msa hvo encoded in the MSI DbRef (lexEntryHvo.refIndex.msaHvo) for an irregularly inflected variant, not just fall back to the variant's first/main sense, since that can be the wrong MSA when the underlying entry has more than one. Co-Authored-By: Claude Sonnet 5 --- .../ParseFilerProcessingTests.cs | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/Src/LexText/ParserCore/ParserCoreTests/ParseFilerProcessingTests.cs b/Src/LexText/ParserCore/ParserCoreTests/ParseFilerProcessingTests.cs index 0237fb41d2..f983d98c7c 100644 --- a/Src/LexText/ParserCore/ParserCoreTests/ParseFilerProcessingTests.cs +++ b/Src/LexText/ParserCore/ParserCoreTests/ParseFilerProcessingTests.cs @@ -13,6 +13,8 @@ using System; using System.Linq; +using System.Reflection; +using System.Xml.Linq; using NUnit.Framework; using SIL.FieldWorks.Common.FwUtils; using SIL.LCModel.Core.Text; @@ -522,6 +524,73 @@ public void LexEntryInflTypeTwoAnalyses() } } + /// ------------------------------------------------------------------------------------ + /// + /// LT-22563: XAmple's MSI DbRef for an irregularly inflected variant is + /// "lexEntryHvo.refIndex.msaHvo" (see LT-22422). TryCreateParseMorph must resolve the + /// specific msaHvo from that DbRef; it must not fall back to just the variant's + /// main/first sense, since that can be a different MSA than XAmple actually chose + /// when the underlying entry has more than one MSA. + /// + /// ------------------------------------------------------------------------------------ + [Test] + public void TryCreateParseMorph_IrregularVariantWithMultipleMsas_UsesMsaFromDbRef() + { + ILexEntry crebV = null; + IMoStemAllomorph crebVForm = null; + IMoStemMsa firstMsa = null; + IMoStemMsa secondMsa = null; + + UndoableUnitOfWorkHelper.Do("Undo stuff", "Redo stuff", m_actionHandler, () => + { + // 'seek' has two senses/MSAs; 'creb' is an irregularly inflected variant of it. + ILexEntryInflType inflType = m_lexEntryInflTypeFactory.Create(); + Cache.LangProject.LexDbOA.VariantEntryTypesOA.PossibilitiesOS.Add(inflType); + + ILexEntry seekV = m_entryFactory.Create(); + IMoStemAllomorph seekVForm = m_stemAlloFactory.Create(); + seekV.AlternateFormsOS.Add(seekVForm); + seekVForm.Form.VernacularDefaultWritingSystem = TsStringUtils.MakeString("seekVTEST", m_vernacularWS.Handle); + firstMsa = m_stemMsaFactory.Create(); + seekV.MorphoSyntaxAnalysesOC.Add(firstMsa); + ILexSense firstSense = m_senseFactory.Create(); + seekV.SensesOS.Add(firstSense); + firstSense.MorphoSyntaxAnalysisRA = firstMsa; + secondMsa = m_stemMsaFactory.Create(); + seekV.MorphoSyntaxAnalysesOC.Add(secondMsa); + ILexSense secondSense = m_senseFactory.Create(); + seekV.SensesOS.Add(secondSense); + secondSense.MorphoSyntaxAnalysisRA = secondMsa; + + crebV = m_entryFactory.Create(); + crebVForm = m_stemAlloFactory.Create(); + crebV.AlternateFormsOS.Add(crebVForm); + crebVForm.Form.VernacularDefaultWritingSystem = TsStringUtils.MakeString("crebVTEST", m_vernacularWS.Handle); + ILexEntryRef lexEntryRef = m_lexEntryRefFactory.Create(); + crebV.EntryRefsOS.Add(lexEntryRef); + lexEntryRef.ComponentLexemesRS.Add(seekV); + lexEntryRef.VariantEntryTypesRS.Add(inflType); + }); + + // Mimic what XAmple echoes back for this kind of variant: MoForm DbRef is the + // allomorph hvo, and MSI DbRef is "lexEntryHvo.refIndex.msaHvo", pointing + // specifically at the *second* MSA of 'seek'. + var morphElem = new XElement("Morph", + new XElement("MoForm", new XAttribute("DbRef", crebVForm.Hvo)), + new XElement("MSI", new XAttribute("DbRef", string.Format("{0}.0.{1}", crebV.Hvo, secondMsa.Hvo)))); + + MethodInfo method = typeof(XAmpleParser).GetMethod("TryCreateParseMorph", BindingFlags.NonPublic | BindingFlags.Static); + var args = new object[] { Cache, morphElem, null }; + var succeeded = (bool) method.Invoke(null, args); + var morph = (ParseMorph) args[2]; + + Assert.That(succeeded, Is.True); + Assert.That(morph, Is.Not.Null); + Assert.That(morph.Msa.Hvo, Is.EqualTo(secondMsa.Hvo), + "Should use the MSA hvo encoded in the MSI DbRef, not just the variant's first/main sense"); + Assert.That(morph.Msa.Hvo, Is.Not.EqualTo(firstMsa.Hvo)); + } + [Test] public void LexEntryInflTypeAnalysisWithNullForSlotFiller() { From 9ab9734fdadf411be5a97837b5bcd2bcbca4c979 Mon Sep 17 00:00:00 2001 From: John Lambert Date: Wed, 8 Jul 2026 11:43:18 -0400 Subject: [PATCH 5/6] Harden LT-22563 regression test with multiple entry refs Extends the DbRef-parsing test to cover a variant with two EntryRefsOS, each targeting a different base entry that itself has two MSAs. This checks both halves of the "lexEntryHvo.refIndex.msaHvo" encoding: the refIndex picking the right LexEntryRef, and the msaHvo picking the right MSA within it, not just the single-ref case. Co-Authored-By: Claude Sonnet 5 --- .../ParseFilerProcessingTests.cs | 112 +++++++++++++++--- 1 file changed, 94 insertions(+), 18 deletions(-) diff --git a/Src/LexText/ParserCore/ParserCoreTests/ParseFilerProcessingTests.cs b/Src/LexText/ParserCore/ParserCoreTests/ParseFilerProcessingTests.cs index f983d98c7c..937d4ce30c 100644 --- a/Src/LexText/ParserCore/ParserCoreTests/ParseFilerProcessingTests.cs +++ b/Src/LexText/ParserCore/ParserCoreTests/ParseFilerProcessingTests.cs @@ -101,6 +101,37 @@ protected void ExecuteIdleQueue() m_idleQueue.Clear(); } + /// + /// Creates a stem entry with two senses, each with its own MoStemMsa, so tests can + /// verify that a specific MSA (not just "the first one") gets resolved. + /// + private ILexEntry CreateStemEntryWithTwoMsas(string form, out IMoStemMsa firstMsa, out IMoStemMsa secondMsa) + { + ILexEntry entry = m_entryFactory.Create(); + IMoStemAllomorph entryForm = m_stemAlloFactory.Create(); + entry.AlternateFormsOS.Add(entryForm); + entryForm.Form.VernacularDefaultWritingSystem = TsStringUtils.MakeString(form, m_vernacularWS.Handle); + firstMsa = m_stemMsaFactory.Create(); + entry.MorphoSyntaxAnalysesOC.Add(firstMsa); + ILexSense firstSense = m_senseFactory.Create(); + entry.SensesOS.Add(firstSense); + firstSense.MorphoSyntaxAnalysisRA = firstMsa; + secondMsa = m_stemMsaFactory.Create(); + entry.MorphoSyntaxAnalysesOC.Add(secondMsa); + ILexSense secondSense = m_senseFactory.Create(); + entry.SensesOS.Add(secondSense); + secondSense.MorphoSyntaxAnalysisRA = secondMsa; + return entry; + } + + private static ParseMorph InvokeTryCreateParseMorph(LcmCache cache, XElement morphElem, out bool succeeded) + { + MethodInfo method = typeof(XAmpleParser).GetMethod("TryCreateParseMorph", BindingFlags.NonPublic | BindingFlags.Static); + var args = new object[] { cache, morphElem, null }; + succeeded = (bool) method.Invoke(null, args); + return (ParseMorph) args[2]; + } + #endregion // Non-tests #region Setup and TearDown @@ -547,20 +578,7 @@ public void TryCreateParseMorph_IrregularVariantWithMultipleMsas_UsesMsaFromDbRe ILexEntryInflType inflType = m_lexEntryInflTypeFactory.Create(); Cache.LangProject.LexDbOA.VariantEntryTypesOA.PossibilitiesOS.Add(inflType); - ILexEntry seekV = m_entryFactory.Create(); - IMoStemAllomorph seekVForm = m_stemAlloFactory.Create(); - seekV.AlternateFormsOS.Add(seekVForm); - seekVForm.Form.VernacularDefaultWritingSystem = TsStringUtils.MakeString("seekVTEST", m_vernacularWS.Handle); - firstMsa = m_stemMsaFactory.Create(); - seekV.MorphoSyntaxAnalysesOC.Add(firstMsa); - ILexSense firstSense = m_senseFactory.Create(); - seekV.SensesOS.Add(firstSense); - firstSense.MorphoSyntaxAnalysisRA = firstMsa; - secondMsa = m_stemMsaFactory.Create(); - seekV.MorphoSyntaxAnalysesOC.Add(secondMsa); - ILexSense secondSense = m_senseFactory.Create(); - seekV.SensesOS.Add(secondSense); - secondSense.MorphoSyntaxAnalysisRA = secondMsa; + ILexEntry seekV = CreateStemEntryWithTwoMsas("seekVTEST", out firstMsa, out secondMsa); crebV = m_entryFactory.Create(); crebVForm = m_stemAlloFactory.Create(); @@ -579,10 +597,8 @@ public void TryCreateParseMorph_IrregularVariantWithMultipleMsas_UsesMsaFromDbRe new XElement("MoForm", new XAttribute("DbRef", crebVForm.Hvo)), new XElement("MSI", new XAttribute("DbRef", string.Format("{0}.0.{1}", crebV.Hvo, secondMsa.Hvo)))); - MethodInfo method = typeof(XAmpleParser).GetMethod("TryCreateParseMorph", BindingFlags.NonPublic | BindingFlags.Static); - var args = new object[] { Cache, morphElem, null }; - var succeeded = (bool) method.Invoke(null, args); - var morph = (ParseMorph) args[2]; + bool succeeded; + ParseMorph morph = InvokeTryCreateParseMorph(Cache, morphElem, out succeeded); Assert.That(succeeded, Is.True); Assert.That(morph, Is.Not.Null); @@ -591,6 +607,66 @@ public void TryCreateParseMorph_IrregularVariantWithMultipleMsas_UsesMsaFromDbRe Assert.That(morph.Msa.Hvo, Is.Not.EqualTo(firstMsa.Hvo)); } + /// ------------------------------------------------------------------------------------ + /// + /// LT-22563: hardens the above test by giving the variant *two* EntryRefsOS (pointing + /// at two different base entries, each itself with two MSAs) and checking both possible + /// refIndex values. This guards both halves of the "lexEntryHvo.refIndex.msaHvo" DbRef: + /// picking the right LexEntryRef by index, and then the right MSA within that ref's + /// target entry -- not just the case where there is only one ref to pick from. + /// + /// ------------------------------------------------------------------------------------ + [TestCase(0)] + [TestCase(1)] + public void TryCreateParseMorph_IrregularVariantWithMultipleEntryRefsAndMsas_UsesRefIndexAndMsaFromDbRef(int entryRefIndex) + { + ILexEntry crebV = null; + IMoStemAllomorph crebVForm = null; + IMoStemMsa believeSecondMsa = null; + IMoStemMsa seekSecondMsa = null; + + UndoableUnitOfWorkHelper.Do("Undo stuff", "Redo stuff", m_actionHandler, () => + { + ILexEntryInflType inflType = m_lexEntryInflTypeFactory.Create(); + Cache.LangProject.LexDbOA.VariantEntryTypesOA.PossibilitiesOS.Add(inflType); + + IMoStemMsa believeFirstMsa; + ILexEntry believeV = CreateStemEntryWithTwoMsas("believeVTEST2", out believeFirstMsa, out believeSecondMsa); + IMoStemMsa seekFirstMsa; + ILexEntry seekV = CreateStemEntryWithTwoMsas("seekVTEST2", out seekFirstMsa, out seekSecondMsa); + + crebV = m_entryFactory.Create(); + crebVForm = m_stemAlloFactory.Create(); + crebV.AlternateFormsOS.Add(crebVForm); + crebVForm.Form.VernacularDefaultWritingSystem = TsStringUtils.MakeString("crebVTEST2", m_vernacularWS.Handle); + + // EntryRefsOS[0] -> believeV, EntryRefsOS[1] -> seekV. + ILexEntryRef believeRef = m_lexEntryRefFactory.Create(); + crebV.EntryRefsOS.Add(believeRef); + believeRef.ComponentLexemesRS.Add(believeV); + believeRef.VariantEntryTypesRS.Add(inflType); + + ILexEntryRef seekRef = m_lexEntryRefFactory.Create(); + crebV.EntryRefsOS.Add(seekRef); + seekRef.ComponentLexemesRS.Add(seekV); + seekRef.VariantEntryTypesRS.Add(inflType); + }); + + IMoStemMsa expectedMsa = entryRefIndex == 0 ? believeSecondMsa : seekSecondMsa; + + var morphElem = new XElement("Morph", + new XElement("MoForm", new XAttribute("DbRef", crebVForm.Hvo)), + new XElement("MSI", new XAttribute("DbRef", string.Format("{0}.{1}.{2}", crebV.Hvo, entryRefIndex, expectedMsa.Hvo)))); + + bool succeeded; + ParseMorph morph = InvokeTryCreateParseMorph(Cache, morphElem, out succeeded); + + Assert.That(succeeded, Is.True); + Assert.That(morph, Is.Not.Null); + Assert.That(morph.Msa.Hvo, Is.EqualTo(expectedMsa.Hvo), + string.Format("Should resolve EntryRefsOS[{0}] and the specific msa hvo encoded in the DbRef", entryRefIndex)); + } + [Test] public void LexEntryInflTypeAnalysisWithNullForSlotFiller() { From f6e1099cf515a97020d68dbc3a569561e227a932 Mon Sep 17 00:00:00 2001 From: John Lambert Date: Wed, 8 Jul 2026 11:50:11 -0400 Subject: [PATCH 6/6] Update stale case-4 doc comment for the 3-part MSI DbRef Case 4 in the TryCreateParseMorph doc comment still described the pre-LT-22422 2-part "y.index" encoding. Update it to describe the current 3-part "y.i.z" format (LexEntry hvo, LexEntryRef index, and the specific MSA hvo), matching what ProcessMsaHvo actually parses. Co-Authored-By: Claude Sonnet 5 --- Src/LexText/ParserCore/XAmpleParser.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Src/LexText/ParserCore/XAmpleParser.cs b/Src/LexText/ParserCore/XAmpleParser.cs index 75c5de2f60..57bde6cbe2 100644 --- a/Src/LexText/ParserCore/XAmpleParser.cs +++ b/Src/LexText/ParserCore/XAmpleParser.cs @@ -245,9 +245,11 @@ private static bool TryCreateParseMorph(LcmCache cache, XElement morphElem, out // required slots in affix templates. The parser filer can ignore these. // 3. ().TryGetObject(int.Parse(formHvo), out objForm)) {