Skip to content

TemplateEnumerator - Optimize parsing of single digit holename#6152

Merged
snakefoot merged 1 commit into
NLog:devfrom
snakefoot:TemplateEnumerator
Apr 17, 2026
Merged

TemplateEnumerator - Optimize parsing of single digit holename#6152
snakefoot merged 1 commit into
NLog:devfrom
snakefoot:TemplateEnumerator

Conversation

@snakefoot
Copy link
Copy Markdown
Contributor

No description provided.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 16, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Constructor now pre-scans for the first {/} and initializes _position/_literalLength. Hole parsing was restructured to always consume the closing }, alignment/format parsing was centralized, and integer parsing was split into ReadSignedInt/ReadUnsignedInt.

Changes

Cohort / File(s) Summary
Template Parsing Refactor
src/NLog/MessageTemplates/TemplateEnumerator.cs
Constructor pre-scans for the first {/} and sets _position/_literalLength. ParseHole now always parses via the ParseName(..., out alignment, out format) path and consumes the closing }. Literal-length/skip calculation adjusted to use _position + literalOffset. ParseName signature changed to ParseName(out int parameterIndex, out int alignment, out string? format) and alignment/format extraction moved to ParseAlignmentAndFormat. Replaced ReadInt with ReadSignedInt and ReadUnsignedInt (new validation/error behavior).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 I hopped through braces, tidy and spry,

Counting digits beneath the sky.
Alignment, format, names in line,
I closed each brace and trimmed the vine.
Templates neat — a carrot-fine.

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive No pull request description was provided by the author, making it impossible to evaluate relevance to the changeset. Add a meaningful description explaining the purpose and benefits of these TemplateEnumerator changes.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately describes the main objectives: fast-forwarding to the first hole and optimizing single-digit hole names in TemplateEnumerator.

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

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

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.

@snakefoot snakefoot force-pushed the TemplateEnumerator branch 2 times, most recently from ba91fa1 to 5b7808f Compare April 16, 2026 19:38
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
src/NLog/MessageTemplates/TemplateEnumerator.cs (2)

65-81: Constructor pre-scan is correct — minor nit on _length reuse.

Traced the pre-scan against the callers in Logger.cs, LogMessageTemplateFormatter.cs, and MessageTemplateParameters.cs, as well as the negative test cases in ParserTests.ThrowsTemplateParserException — all exception paths still fire via MoveNext, and the literal-only and empty-template cases degrade gracefully.

Small consistency nit: the else branch reassigns _template.Length twice when _length is already cached one line above.

♻️ Optional consistency tweak
             else
             {
-                _position = _template.Length;
-                _literalLength = _template.Length;
+                _position = _length;
+                _literalLength = _length;
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/NLog/MessageTemplates/TemplateEnumerator.cs` around lines 65 - 81, The
constructor in TemplateEnumerator caches _length but the else branch redundantly
calls _template.Length again; update the else branch to use the cached _length
when assigning _position and _literalLength to avoid inconsistent access
patterns—edit the TemplateEnumerator(string template) constructor to set
_position = _length and _literalLength = _length (referencing the _length field,
not _template.Length) while leaving the existing first-branch logic and other
fields (_template, _position, _literalLength, _current) unchanged.

374-411: Signed/unsigned split looks good; note silent overflow beyond 10 digits.

The contract (caller guarantees c is '0'..'9' before invoking ReadUnsignedInt) is honored by both call sites (ReadSignedInt and the positional branch of ParseName). The 12-iteration cap yields up to 13 consumed digits before throwing — values in the 11–13 digit range silently wrap (int overflow is unchecked). This matches the prior ReadInt behavior in practice, so it is not blocking, but a checked block (or tightening the cap to 10) would produce a clearer TemplateParserException for pathological inputs instead of a garbage index/alignment.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/NLog/MessageTemplates/TemplateEnumerator.cs` around lines 374 - 411,
ReadUnsignedInt can silently overflow for 11–13 digit inputs; update it so it
fails with a TemplateParserException instead of producing a wrapped/garbage int.
Either (a) enforce a max of 10 digits (first digit is handled before the loop —
e.g. loop at most 9 more digits) and throw TemplateParserException when more
digits are present, or (b) perform the multiplication/add in a checked context
and catch OverflowException to rethrow as TemplateParserException. Apply this
change in ReadUnsignedInt (and ensure ReadSignedInt behavior remains consistent)
so pathological numeric token lengths are rejected deterministically.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/NLog/MessageTemplates/TemplateEnumerator.cs`:
- Around line 65-81: The constructor in TemplateEnumerator caches _length but
the else branch redundantly calls _template.Length again; update the else branch
to use the cached _length when assigning _position and _literalLength to avoid
inconsistent access patterns—edit the TemplateEnumerator(string template)
constructor to set _position = _length and _literalLength = _length (referencing
the _length field, not _template.Length) while leaving the existing first-branch
logic and other fields (_template, _position, _literalLength, _current)
unchanged.
- Around line 374-411: ReadUnsignedInt can silently overflow for 11–13 digit
inputs; update it so it fails with a TemplateParserException instead of
producing a wrapped/garbage int. Either (a) enforce a max of 10 digits (first
digit is handled before the loop — e.g. loop at most 9 more digits) and throw
TemplateParserException when more digits are present, or (b) perform the
multiplication/add in a checked context and catch OverflowException to rethrow
as TemplateParserException. Apply this change in ReadUnsignedInt (and ensure
ReadSignedInt behavior remains consistent) so pathological numeric token lengths
are rejected deterministically.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 9ef4dae2-55cf-4544-8edb-ac9eb4c9d1ab

📥 Commits

Reviewing files that changed from the base of the PR and between 96f6413 and ba91fa1.

📒 Files selected for processing (1)
  • src/NLog/MessageTemplates/TemplateEnumerator.cs

@snakefoot snakefoot force-pushed the TemplateEnumerator branch 2 times, most recently from 72bc448 to 8023404 Compare April 16, 2026 19:44
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/NLog/MessageTemplates/TemplateEnumerator.cs (1)

392-410: ⚠️ Potential issue | 🟡 Minor

Misleading error message and silent overflow for very long digit runs.

Two small concerns on the rewritten helper:

  1. After consuming 12 valid digits, the loop exits with a 13th digit in c and throws TemplateParserException("An integer is expected", …). At that point digits were found; the real condition is "integer too long / overflow". The message will confuse users who hit this with a long numeric hole name like {1234567890123}.
  2. Because i = i * 10 + (c - '0') is unchecked int arithmetic, any 11–12 digit numeric index silently wraps before we reach the length check (e.g. "999999999999" parses to a negative value and is then cast to short).

Consider tightening the message and/or using a digit-count/overflow guard.

🛡️ Suggested message/overflow tightening
-            throw new TemplateParserException("An integer is expected", _position, _template);
+            throw new TemplateParserException("Integer is too large or malformed", _position, _template);

Or use checked { i = i * 10 + (c - '0'); } and translate OverflowException into a TemplateParserException.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/NLog/MessageTemplates/TemplateEnumerator.cs` around lines 392 - 410, In
ReadUnsignedInt, the code currently throws a misleading "An integer is expected"
after consuming many digits and also allows silent int overflow; update the
method to detect and report "integer too long" (or "integer overflow") instead
of the current message and prevent/translate overflow: either perform the
accumulation inside a checked block and catch OverflowException to rethrow a
TemplateParserException with a clear "integer too long/overflow" message
(include _position and _template), or add an explicit digit-count/limit guard
before multiplying so you throw TemplateParserException("Integer too long",
_position, _template) when the value would exceed int range; keep references to
ReadUnsignedInt, TemplateParserException, _position and _template so the error
is raised with correct context.
🧹 Nitpick comments (2)
src/NLog/MessageTemplates/TemplateEnumerator.cs (2)

192-206: Losing the Skip('}') invariant check.

Replacing Skip('}') with a bare _position++ drops the Debug.Assert(_template[_position] == '}') that guarded the parser invariant "after ParseName/format parsing we are sitting on }". Given every path leading here (format parser unread, direct }/: peek, alignment+}) is supposed to land exactly on }, keeping the assert has zero release-build cost and would catch regressions in future refactors.

♻️ Retain the debug-only invariant check
-            _position++; // Consume closing '}'
+            Skip('}'); // Consume closing '}' (Debug.Assert validates the invariant)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/NLog/MessageTemplates/TemplateEnumerator.cs` around lines 192 - 206, The
replacement of Skip('}') with a bare _position++ in ParseHole removed the
debug-only invariant that ensures the parser is positioned on '}' after
ParseName/format parsing; restore the guard by asserting the current character
is '}' before advancing (e.g., reintroduce Debug.Assert(_template[_position] ==
'}') or call the existing Skip('}') helper) so ParseHole (and its usage of
_position++/Skip) re-establishes the invariant and then increments the position.

103-108: Reset() no longer mirrors the constructor's fast-forward.

After this change, Reset() leaves _position = 0, _literalLength = 0, so the first MoveNext() after a reset will re-scan via ParseTextPart instead of jumping straight to the first delimiter. This is functionally correct but gives up the optimization you just added for the reset path. Consider either reusing the same pre-scan, or documenting that Reset() is a slow-path reset.

♻️ Optional alignment with constructor
 public void Reset()
 {
-    _position = 0;
-    _literalLength = 0;
     _current = default(LiteralHole);
+    var firstDelimiter = _template.IndexOfAny(TextDelimiters);
+    _position = firstDelimiter >= 0 ? firstDelimiter : _length;
+    _literalLength = _position;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/NLog/MessageTemplates/TemplateEnumerator.cs` around lines 103 - 108,
Reset() currently sets _position=0 and _literalLength=0 which forces the next
MoveNext() to re-scan via ParseTextPart and loses the constructor's fast-forward
optimization; update Reset() in TemplateEnumerator to re-run the same initial
pre-scan the constructor performs (or call the same initialization helper) so
_position/_literalLength/_current are advanced to the first delimiter like the
constructor does, or explicitly document that Reset() is a slow-path reset;
reference Reset(), the constructor, MoveNext(), and ParseTextPart when making
the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/NLog/MessageTemplates/TemplateEnumerator.cs`:
- Around line 392-410: In ReadUnsignedInt, the code currently throws a
misleading "An integer is expected" after consuming many digits and also allows
silent int overflow; update the method to detect and report "integer too long"
(or "integer overflow") instead of the current message and prevent/translate
overflow: either perform the accumulation inside a checked block and catch
OverflowException to rethrow a TemplateParserException with a clear "integer too
long/overflow" message (include _position and _template), or add an explicit
digit-count/limit guard before multiplying so you throw
TemplateParserException("Integer too long", _position, _template) when the value
would exceed int range; keep references to ReadUnsignedInt,
TemplateParserException, _position and _template so the error is raised with
correct context.

---

Nitpick comments:
In `@src/NLog/MessageTemplates/TemplateEnumerator.cs`:
- Around line 192-206: The replacement of Skip('}') with a bare _position++ in
ParseHole removed the debug-only invariant that ensures the parser is positioned
on '}' after ParseName/format parsing; restore the guard by asserting the
current character is '}' before advancing (e.g., reintroduce
Debug.Assert(_template[_position] == '}') or call the existing Skip('}') helper)
so ParseHole (and its usage of _position++/Skip) re-establishes the invariant
and then increments the position.
- Around line 103-108: Reset() currently sets _position=0 and _literalLength=0
which forces the next MoveNext() to re-scan via ParseTextPart and loses the
constructor's fast-forward optimization; update Reset() in TemplateEnumerator to
re-run the same initial pre-scan the constructor performs (or call the same
initialization helper) so _position/_literalLength/_current are advanced to the
first delimiter like the constructor does, or explicitly document that Reset()
is a slow-path reset; reference Reset(), the constructor, MoveNext(), and
ParseTextPart when making the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: b1c91489-a4d1-4915-b54f-316eaec7ec81

📥 Commits

Reviewing files that changed from the base of the PR and between ba91fa1 and 72bc448.

📒 Files selected for processing (1)
  • src/NLog/MessageTemplates/TemplateEnumerator.cs

@snakefoot snakefoot force-pushed the TemplateEnumerator branch 2 times, most recently from 1397b50 to f62a4e4 Compare April 16, 2026 19:51
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/NLog/MessageTemplates/TemplateEnumerator.cs (1)

400-410: ⚠️ Potential issue | 🟡 Minor

Misleading exception message on the >10-digit overflow guard.

When this throw fires, the issue isn't "an integer is expected" (we've already consumed ≥10 digits) — it's that the integer is too large / has too many digits. Consider differentiating the message from the "no digit found" case at Line 386 to aid diagnosis of malformed templates like {12345678901}.

✏️ Suggested message tweak
-            throw new TemplateParserException("An integer is expected", _position, _template);
+            throw new TemplateParserException("Integer value is too large", _position, _template);

Side note (not introduced by this PR, flagging for awareness): i = i * 10 + (c - '0') can silently overflow int on 10-digit values exceeding int.MaxValue (e.g., 9999999999) before the 11-digit guard trips. Unlikely to matter for realistic parameter indices/alignments, so fine to leave as-is.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/NLog/MessageTemplates/TemplateEnumerator.cs` around lines 400 - 410, The
thrown TemplateParserException at the 10-digit overflow guard should indicate
the value is too large rather than "An integer is expected"; update the throw in
TemplateEnumerator (the block using i = i * 10 + (c - '0'), _position, Peek(),
and _template) to use a distinct message like "Integer value too large or too
many digits" (or similar) so it differs from the earlier "no digit found" case,
keeping the same exception type and passing _position and _template.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/NLog/MessageTemplates/TemplateEnumerator.cs`:
- Around line 400-410: The thrown TemplateParserException at the 10-digit
overflow guard should indicate the value is too large rather than "An integer is
expected"; update the throw in TemplateEnumerator (the block using i = i * 10 +
(c - '0'), _position, Peek(), and _template) to use a distinct message like
"Integer value too large or too many digits" (or similar) so it differs from the
earlier "no digit found" case, keeping the same exception type and passing
_position and _template.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 5e2a2422-ad99-47d8-aee1-2bde20a9eecb

📥 Commits

Reviewing files that changed from the base of the PR and between 72bc448 and 8023404.

📒 Files selected for processing (1)
  • src/NLog/MessageTemplates/TemplateEnumerator.cs

@snakefoot snakefoot force-pushed the TemplateEnumerator branch 2 times, most recently from 8b283d7 to 21d5a88 Compare April 16, 2026 19:55
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/NLog/MessageTemplates/TemplateEnumerator.cs (1)

103-108: Optional: mirror the constructor's fast-forward in Reset().

After Reset() the enumerator is left at _position = 0, _literalLength = 0 rather than fast-forwarded to the first delimiter, so the first MoveNext after a Reset takes the slower ParseTextPart path. Functionally correct (behavior is identical), but it leaves the two entry points semantically out of sync. If you want to keep them aligned, factor the init into a shared helper.

♻️ Possible alignment
         public void Reset()
         {
-            _position = 0;
-            _literalLength = 0;
             _current = default(LiteralHole);
+            var firstDelimiter = _template.IndexOfAny(TextDelimiters);
+            if (firstDelimiter >= 0)
+            {
+                _position = firstDelimiter;
+                _literalLength = firstDelimiter;
+            }
+            else
+            {
+                _position = _length;
+                _literalLength = _length;
+            }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/NLog/MessageTemplates/TemplateEnumerator.cs` around lines 103 - 108,
Reset() currently just zeroes _position/_literalLength/_current leaving the
enumerator before the first delimiter so the first MoveNext() hits
ParseTextPart; update Reset() to mirror the constructor’s fast-forward logic by
extracting the initial fast-forward into a shared helper (e.g., Initialize or
FastForwardToDelimiter) and call it from both the constructor and Reset(),
ensuring Reset(), the constructor, and MoveNext()/ParseTextPart are semantically
aligned while preserving existing fields (_position, _literalLength, _current,
LiteralHole).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/NLog/MessageTemplates/TemplateEnumerator.cs`:
- Around line 103-108: Reset() currently just zeroes
_position/_literalLength/_current leaving the enumerator before the first
delimiter so the first MoveNext() hits ParseTextPart; update Reset() to mirror
the constructor’s fast-forward logic by extracting the initial fast-forward into
a shared helper (e.g., Initialize or FastForwardToDelimiter) and call it from
both the constructor and Reset(), ensuring Reset(), the constructor, and
MoveNext()/ParseTextPart are semantically aligned while preserving existing
fields (_position, _literalLength, _current, LiteralHole).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: a13dfc25-7dc0-4e80-a126-bdb020bd8e74

📥 Commits

Reviewing files that changed from the base of the PR and between 8023404 and f62a4e4.

📒 Files selected for processing (1)
  • src/NLog/MessageTemplates/TemplateEnumerator.cs

@snakefoot snakefoot closed this Apr 17, 2026
@snakefoot snakefoot reopened this Apr 17, 2026
@snakefoot snakefoot closed this Apr 17, 2026
@snakefoot snakefoot reopened this Apr 17, 2026
@snakefoot snakefoot force-pushed the TemplateEnumerator branch from 21d5a88 to 2116cde Compare April 17, 2026 05:07
@snakefoot snakefoot closed this Apr 17, 2026
@snakefoot snakefoot reopened this Apr 17, 2026
@sonarqubecloud
Copy link
Copy Markdown

@sonarqubecloud
Copy link
Copy Markdown

@snakefoot snakefoot merged commit f1c6b69 into NLog:dev Apr 17, 2026
6 of 7 checks passed
@snakefoot snakefoot added this to the 6.1.3 milestone May 4, 2026
@snakefoot snakefoot changed the title TemplateEnumerator - Fast forward to first hole, and optimize for single digit holename TemplateEnumerator - Optimize parsing of single digit holename May 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant