fix VerseRef.chapterNum setter infinite recursion - #58
Merged
Conversation
The setter assigned to itself (`this.chapterNum = value`) instead of the
backing field, so any assignment recursed until `RangeError: Maximum call
stack size exceeded`. It was marked with a `ToDo` placeholder.
Ported from the C# `VerseRef.ChapterNum` setter, which guards against
negative values before assigning:
if (value < 0)
throw new VerseRefException("ChapterNum can not be negative");
chapterNum = (short)value;
Tests port `BuildVerseRefByProps` from SIL.Scripture.Tests, which builds a
VerseRef entirely through property setters and so covers this directly. One
assertion is commented out: chapter 0 is out of range in the C#, but
`internalValid()` cannot detect that until the versification port lands.
Also adds TS-only tests for the negative guard and the zero boundary, since
the C# has no direct test for the setter's validation.
The `-1` sentinel path is unaffected: `setEmpty()` and the `chapter` string
setter write the backing field directly, exactly as in the C#.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #58 +/- ##
==========================================
+ Coverage 81.40% 83.93% +2.53%
==========================================
Files 4 4
Lines 328 330 +2
Branches 76 77 +1
==========================================
+ Hits 267 277 +10
+ Misses 39 33 -6
+ Partials 22 20 -2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
irahopkinson
added a commit
that referenced
this pull request
Aug 10, 2026
The setter assigned the backing field and nothing else, marked with a `ToDo`
placeholder. It was missing both of the other things the C# `VerseRef.VerseNum`
setter does:
if (value < 0)
throw new VerseRefException("VerseNum can not be negative");
verseNum = (short)value;
verse = null;
So negative verse numbers were accepted, and assigning `verseNum` left the
range/segment string in place. Setting `verseNum = 9` on `LUK 3:4b-5a` left a
stale `4b-5a` in the `verse` getter and `hasMultiple` still `true`.
Uses `this._verse = undefined` rather than `null`, per the repo convention of
preferring `undefined` for missing values. The C# `(short)` cast is not
replicated.
The C# test that covers the range-clearing is `CopyVerseFrom`, which sets
`VerseNum = 9` on a `LUK 3:4b-6a` source and then asserts the copied `Verse` is
`"9"`. That test cannot be ported yet — `copyVerseFrom` is not implemented in
this port — so the cases are added as TS-only tests instead: the negative
guard, the zero boundary, and the clearing of both a range and a segment.
`BuildVerseRefByProps` already exercised `verseNum = 0/15/17` and still passes;
those are plain numbers with no verse string to clear.
Behaviour change for consumers: unlike the sibling `chapterNum` setter fixed in
#58, this setter worked before, so code reading `verse`/`hasMultiple` after
assigning `verseNum` will see different results.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
irahopkinson
added a commit
that referenced
this pull request
Aug 10, 2026
The setter assigned the backing field and nothing else, marked with a `ToDo`
placeholder. It was missing both of the other things the C# `VerseRef.VerseNum`
setter does:
if (value < 0)
throw new VerseRefException("VerseNum can not be negative");
verseNum = (short)value;
verse = null;
So negative verse numbers were accepted, and assigning `verseNum` left the
range/segment string in place. Setting `verseNum = 9` on `LUK 3:4b-5a` left a
stale `4b-5a` in the `verse` getter and `hasMultiple` still `true`.
Uses `this._verse = undefined` rather than `null`, per the repo convention of
preferring `undefined` for missing values. The C# `(short)` cast is not
replicated.
The C# test that covers the range-clearing is `CopyVerseFrom`, which sets
`VerseNum = 9` on a `LUK 3:4b-6a` source and then asserts the copied `Verse` is
`"9"`. That test cannot be ported yet — `copyVerseFrom` is not implemented in
this port — so the cases are added as TS-only tests instead: the negative
guard, the zero boundary, and the clearing of both a range and a segment.
`BuildVerseRefByProps` already exercised `verseNum = 0/15/17` and still passes;
those are plain numbers with no verse string to clear.
Behaviour change for consumers: unlike the sibling `chapterNum` setter fixed in
#58, this setter worked before, so code reading `verse`/`hasMultiple` after
assigning `verseNum` will see different results.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
irahopkinson
added a commit
to eten-tech-foundation/scripture-editors
that referenced
this pull request
Aug 25, 2026
Routine minor bump, not a security update: the advisory-ID set is unchanged (GHSA-g2r8-wvmj-jf5w, GHSA-vp3h-ghgh-jr7g before and after), so this neither closes nor opens a finding. No overrides needed. Skips 2.0.6, so it also picks up the `VerseRef.chapterNum` setter infinite-recursion fix (sillsdev/scripture#58). 2.1.0 is flagged upstream as a potential breaking change: the `VerseRef.verseNum` setter was a mis-port that assigned the backing field and nothing else, and now also clears the verse string and throws on negative values, matching the C# `VerseRef.VerseNum` it ports. We are not affected. That is an instance setter on the `VerseRef` class, and outside the vendored `demos/platform/lib` copy this repo never touches the class — only `SerializedVerseRef`, an interface whose `verseNum` is a plain data property with no setter. The two `new VerseRef(...)` sites in platform-bible-utils' scripture-util.ts are read-only expressions that never store the instance, and every `verseNum`/`chapterNum` assignment in the tree targets a plain object or a local number. The upstream release notes survey this repo by name and reach the same conclusion. 2.1.0 declares no dependencies, so the lockfile diff is confined to the three specifiers and the single package entry. Verified with `nx run-many -t build test lint typecheck --skip-nx-cache` across all 10 projects: green, 1793 tests passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
irahopkinson
added a commit
to eten-tech-foundation/scripture-editors
that referenced
this pull request
Aug 25, 2026
Routine minor bump, not a security update: the advisory-ID set is unchanged (GHSA-g2r8-wvmj-jf5w, GHSA-vp3h-ghgh-jr7g before and after), so this neither closes nor opens a finding. No overrides needed. Skipping 2.0.6 means this picks up two `VerseRef` setter repairs at once. Neither changes our behaviour today — we hold no `VerseRef` instance to assign to — but both were armed traps that would have fired the first time we reached for that API, so taking the bump disarms them ahead of the need rather than after it. `chapterNum` (2.0.6, sillsdev/scripture#58) assigned the property to itself behind a `ToDo` placeholder and recursed infinitely. There was no guard in front of it, so every input threw `RangeError: Maximum call stack size exceeded` — `= 5`, `= 0`, `= -1` alike. The property was completely unusable. `verseNum` (2.1.0, sillsdev/scripture#60) is the same class of mis-port, and is flagged upstream as a potential breaking change because it did work. It assigned the backing field and nothing else, where the C# it ports also clears the verse string and rejects negatives. So `new VerseRef('LUK','3','4b-5a')` then `verseNum = 9` left `verse` reading '4b-5a' and `hasMultiple` true, describing the old reference while `verseNum` described the new one. Of the two, `verseNum` was the more dangerous to us: the recursion fails loud on first use, while the stale string fails silent and flows onward into anything reading `verse` or `toString()`. We reach neither today. Both are instance setters on the `VerseRef` class, and outside the vendored `demos/platform/lib` copy this repo never touches the class — only `SerializedVerseRef`, an interface whose `verseNum` is a plain data property with no setter. The two `new VerseRef(...)` sites in platform-bible-utils' scripture-util.ts are read-only expressions that never store the instance, and every `verseNum`/`chapterNum` assignment in the tree targets a plain object or a local number. The upstream release notes survey this repo by name and reach the same conclusion. 2.1.0 declares no dependencies, so the lockfile diff is confined to the three specifiers and the single package entry. Verified with `nx run-many -t build test lint typecheck --skip-nx-cache` across all 10 projects: green, 1793 tests passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
irahopkinson
added a commit
to eten-tech-foundation/scripture-editors
that referenced
this pull request
Aug 27, 2026
Routine minor bump, not a security update: the advisory-ID set is unchanged (GHSA-g2r8-wvmj-jf5w, GHSA-vp3h-ghgh-jr7g before and after), so this neither closes nor opens a finding. No overrides needed. Skipping 2.0.6 means this picks up two `VerseRef` setter repairs at once. Neither changes our behaviour today — we hold no `VerseRef` instance to assign to — but both were armed traps that would have fired the first time we reached for that API, so taking the bump disarms them ahead of the need rather than after it. `chapterNum` (2.0.6, sillsdev/scripture#58) assigned the property to itself behind a `ToDo` placeholder and recursed infinitely. There was no guard in front of it, so every input threw `RangeError: Maximum call stack size exceeded` — `= 5`, `= 0`, `= -1` alike. The property was completely unusable. `verseNum` (2.1.0, sillsdev/scripture#60) is the same class of mis-port, and is flagged upstream as a potential breaking change because it did work. It assigned the backing field and nothing else, where the C# it ports also clears the verse string and rejects negatives. So `new VerseRef('LUK','3','4b-5a')` then `verseNum = 9` left `verse` reading '4b-5a' and `hasMultiple` true, describing the old reference while `verseNum` described the new one. Of the two, `verseNum` was the more dangerous to us: the recursion fails loud on first use, while the stale string fails silent and flows onward into anything reading `verse` or `toString()`. We reach neither today. Both are instance setters on the `VerseRef` class, and outside the vendored `demos/platform/lib` copy this repo never touches the class — only `SerializedVerseRef`, an interface whose `verseNum` is a plain data property with no setter. The two `new VerseRef(...)` sites in platform-bible-utils' scripture-util.ts are read-only expressions that never store the instance, and every `verseNum`/`chapterNum` assignment in the tree targets a plain object or a local number. The upstream release notes survey this repo by name and reach the same conclusion. 2.1.0 declares no dependencies, so the lockfile diff is confined to the three specifiers and the single package entry. Verified with `nx run-many -t build test lint typecheck --skip-nx-cache` across all 10 projects: green, 1793 tests passed. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the
VerseRef.chapterNumsetter, which recursed infinitely on every assignment. Built test-first: the ported C# test was confirmed failing withRangeErrorbefore the fix went in.The bug
src/verse-ref.tsassigned the property to itself instead of the backing field, behind aToDoplaceholder:There's no guard in front of it, so every input threw
RangeError: Maximum call stack size exceeded—= 5,= 0,= -1alike. The property was completely unusable.Nothing inside the library was affected: the numeric constructor writes
_chapterNumdirectly (verse-ref.ts:244), andsetEmpty()and thechapterstring setter do the same. That line was the only write through the setter, so this was reachable only by external consumers.The fix
Ported from the C#
VerseRef.ChapterNum, which guards negatives then assigns:giving:
This mirrors the existing
bookNumsetter, which already ports its C# counterpart's throw verbatim. The(short)cast is deliberately not replicated — TS has noshort, and emulating 16-bit wraparound would be fidelity at the cost of sense.The
-1sentinel path is untouched.setEmpty()andset chapter('')write_chapterNumdirectly, so the new guard never intercepts them — exactly as in the C#, whereChaptersets the field to-1without going throughChapterNum.Chapter and Verse as Empty Stringsstill passes.Tests
Ports
BuildVerseRefByPropsfromSIL.Scripture.Tests, which builds aVerseRefentirely through property setters and so covers this path directly.One assertion is commented out. After
bookNum = 13, the C# expectsOutOfRangebecause chapter 0 is invalid, butinternalValid()has its chapter/verse range check commented out pending the versification port (see theTODOatverse-ref.ts:623), so it returnsValid. Commented rather than weakened, following the existing convention in this file, and it should be restored when that port lands. The other fivevalidStatus/validassertions in the ported test pass legitimately.Also adds two TS-only tests under
Extra (TS-only tests)for the negative guard and the zero boundary, since the C# has no direct test for the setter's validation.Is this a behaviour change?
Technically yes, but not a breaking one. Since every prior assignment crashed, there is no code path that worked before and behaves differently now — this is a patch-level repair of a property that was 100% unusable. The public API surface is unchanged; the
.d.tsdeclarations forchapterNumare identical.The only way to have depended on the old behaviour was catching the stack overflow as control flow, and even that largely survives: negatives still throw, now as
VerseRefExceptionrather thanRangeError.Verification
npm run test:ci— 43 passed (was 40)npm run lint— cleannpm run prettier:ci— cleannpm run build— cleanRed was confirmed twice before the fix: first
RangeErrorfrom the recursion for the ported test, thenexpected function to throw an error, but it didn'tfor the negative guard.Follow-up
set verseNumis the same class of mis-port and still carries itsToDo. The C# is:The TS does a bare
this._verseNum = value— missing both the guard and theverse = null. Sonew VerseRef('LUK','3','4b-5a')thenverseNum = 9leaves a stale'4b-5a'in theversegetter. Deliberately left out of this PR: unlikechapterNum, that setter works today, so fixing it changes the observable output of working consumer code and deserves its own review.🤖 Generated with Claude Code