diff --git a/src/TopDownProteomics/ProForma/ProFormaParser.cs b/src/TopDownProteomics/ProForma/ProFormaParser.cs
index 7943c50..c380a97 100644
--- a/src/TopDownProteomics/ProForma/ProFormaParser.cs
+++ b/src/TopDownProteomics/ProForma/ProFormaParser.cs
@@ -440,16 +440,77 @@ private void ProcessTag(string tag, int? startIndex, int index, bool inSequenceA
"gno" => Tuple.Create(ProFormaKey.Identifier, ProFormaEvidenceType.Gno, text, groupName, weight),
// Handle names and masses
- "u" => Tuple.Create(getKey(isMass), ProFormaEvidenceType.Unimod, text.Substring(colon + 1), groupName, weight),
- "m" => Tuple.Create(getKey(isMass), ProFormaEvidenceType.PsiMod, text.Substring(colon + 1), groupName, weight),
- "r" => Tuple.Create(getKey(isMass), ProFormaEvidenceType.Resid, text.Substring(colon + 1), groupName, weight),
- "x" => Tuple.Create(getKey(isMass), ProFormaEvidenceType.XlMod, text.Substring(colon + 1), groupName, weight),
- "g" => Tuple.Create(getKey(isMass), ProFormaEvidenceType.Gno, text.Substring(colon + 1), groupName, weight),
- "b" => Tuple.Create(getKey(isMass), ProFormaEvidenceType.Brno, text.Substring(colon + 1), groupName, weight),
- "obs" => Tuple.Create(getKey(isMass), ProFormaEvidenceType.Observed, text.Substring(colon + 1), groupName, weight),
+ "u" => this.CreateNameOrMass(isMass, ProFormaEvidenceType.Unimod, text.Substring(colon + 1), groupName, weight),
+ "m" => this.CreateNameOrMass(isMass, ProFormaEvidenceType.PsiMod, text.Substring(colon + 1), groupName, weight),
+ "r" => this.CreateNameOrMass(isMass, ProFormaEvidenceType.Resid, text.Substring(colon + 1), groupName, weight),
+ "x" => this.CreateNameOrMass(isMass, ProFormaEvidenceType.XlMod, text.Substring(colon + 1), groupName, weight),
+ "g" => this.CreateNameOrMass(isMass, ProFormaEvidenceType.Gno, text.Substring(colon + 1), groupName, weight),
+ "b" => this.CreateNameOrMass(isMass, ProFormaEvidenceType.Brno, text.Substring(colon + 1), groupName, weight),
+ "obs" => this.CreateNameOrMass(isMass, ProFormaEvidenceType.Observed, text.Substring(colon + 1), groupName, weight),
_ => Tuple.Create(ProFormaKey.Name, ProFormaEvidenceType.None, text, groupName, weight)
};
}
+
+ ///
+ /// Creates a name or mass descriptor for a short ontology source prefix (U:, M:, R:, ...).
+ /// A short prefix denotes a modification NAME; when the value is written in the accession
+ /// format of its ontology (e.g. "U:35" or "R:AA0581") the full accession prefix
+ /// (UNIMOD:, MOD:, RESID:) was intended. ProForma 2.0 lists such forms as invalid, so they
+ /// are rejected here rather than silently treated as a (non-existent) name.
+ ///
+ private Tuple CreateNameOrMass(
+ bool isMass, ProFormaEvidenceType evidenceType, string value, string? groupName, double weight)
+ {
+ ProFormaKey key = isMass ? ProFormaKey.Mass : ProFormaKey.Name;
+
+ if (key == ProFormaKey.Name)
+ {
+ string? accessionPrefix = GetMisusedAccessionPrefix(evidenceType, value);
+
+ if (accessionPrefix != null)
+ throw new ProFormaParseException(
+ $"'{value}' is in {accessionPrefix} accession format; use the full accession " +
+ $"prefix (e.g. [{accessionPrefix}:{value}]) instead of the short source prefix.");
+ }
+
+ return Tuple.Create(key, evidenceType, value, groupName, weight);
+ }
+
+ ///
+ /// Returns the full accession prefix a value should have used when it is written in the
+ /// accession format of its ontology under a short source prefix, or null otherwise.
+ ///
+ private static string? GetMisusedAccessionPrefix(ProFormaEvidenceType evidenceType, string value)
+ {
+ static bool IsAllDigits(string s)
+ {
+ if (s.Length == 0)
+ return false;
+ foreach (char c in s)
+ if (!char.IsDigit(c))
+ return false;
+ return true;
+ }
+
+ static bool IsResidAccession(string s)
+ {
+ // RESID accessions are "AA" followed by digits, e.g. AA0581.
+ if (s.Length < 3 || s[0] != 'A' || s[1] != 'A')
+ return false;
+ for (int i = 2; i < s.Length; i++)
+ if (!char.IsDigit(s[i]))
+ return false;
+ return true;
+ }
+
+ return evidenceType switch
+ {
+ ProFormaEvidenceType.Unimod when IsAllDigits(value) => "UNIMOD",
+ ProFormaEvidenceType.PsiMod when IsAllDigits(value) => "MOD",
+ ProFormaEvidenceType.Resid when IsResidAccession(value) => "RESID",
+ _ => null
+ };
+ }
}
}
\ No newline at end of file
diff --git a/tests/TopDownProteomics.Tests/ProForma/ProFormaParserTests.cs b/tests/TopDownProteomics.Tests/ProForma/ProFormaParserTests.cs
index ffddd5c..5e3094e 100644
--- a/tests/TopDownProteomics.Tests/ProForma/ProFormaParserTests.cs
+++ b/tests/TopDownProteomics.Tests/ProForma/ProFormaParserTests.cs
@@ -1144,5 +1144,25 @@ public void UnimodCappedTest(string proFormaString, ProFormaEvidenceType modType
Assert.AreEqual("UNIMOD:15", desc1.Value);
}
#endregion
+
+ [Test]
+ public void RejectAccessionUnderShortSourcePrefix()
+ {
+ // ProForma 2.0: an ontology accession must use the full prefix (UNIMOD:/MOD:/RESID:).
+ // Writing the accession under the short name prefix (U:/M:/R:) is invalid.
+ Assert.Throws(() => _parser.ParseString("EM[U:35]EVEES[U:56]PEK"));
+ Assert.Throws(() => _parser.ParseString("EM[M:00719]EVEES[M:00046]PEK"));
+ Assert.Throws(() => _parser.ParseString("EM[R:AA0581]EVEES[R:AA0037]PEK"));
+ }
+
+ [Test]
+ public void AllowNamesAndMassesUnderShortSourcePrefix()
+ {
+ // Real names and source-tagged masses under the short prefix remain valid.
+ Assert.DoesNotThrow(() => _parser.ParseString("EM[U:Oxidation]EVEES[U:Phospho]PEK"));
+ Assert.DoesNotThrow(() => _parser.ParseString("EM[M:L-methionine sulfoxide]EVEES[M:O-phospho-L-serine]PEK"));
+ Assert.DoesNotThrow(() => _parser.ParseString("EM[U:+15.9949]EVEES[U:+79.9663]PEK"));
+ Assert.DoesNotThrow(() => _parser.ParseString("EM[R: L-methionine sulfone]EVEESPEK"));
+ }
}
}
\ No newline at end of file