Skip to content

Comments

refactor: Extract Parser and Emitter to internal top-level classes#56

Merged
DrBarnabus merged 1 commit intomainfrom
refactor/extract-parser-emitter-to-internal-classes
Feb 23, 2026
Merged

refactor: Extract Parser and Emitter to internal top-level classes#56
DrBarnabus merged 1 commit intomainfrom
refactor/extract-parser-emitter-to-internal-classes

Conversation

@DrBarnabus
Copy link
Owner

@DrBarnabus DrBarnabus commented Feb 23, 2026

Summary

  • Extract Parser and Emitter from private sealed nested classes within SourceGenerator to internal sealed top-level classes, enabling direct unit testing in isolation
  • Add InternalsVisibleTo for the test project with necessary PolySharp polyfill exclusion and assembly reference disambiguation
  • Pure structural refactor with no behavioral changes — all snapshot tests and incremental caching tests pass identically

Changes

  • Parser.cs — Extracted from SourceGenerator.Parser.cs as internal sealed class Parser at namespace level
  • Emitter.cs — Extracted from SourceGenerator.Emitter.cs as internal sealed class Emitter at namespace level
  • SourceGenerator.cs — Removed partial modifier, moved CompositeKeyAttributeFullName constant inline
  • CompositeKey.SourceGeneration.csproj — Added InternalsVisibleTo for UnitTests project; excluded PolySharp ModuleInitializerAttribute polyfill to prevent type conflicts exposed via InternalsVisibleTo
  • CompilationHelper.cs — Resolved CompositeKeyAttribute assembly reference by name to avoid CS0433 ambiguity between CompositeKey and CompositeKey.SourceGeneration assemblies

Closes #41

Summary by CodeRabbit

  • Refactor
    • Reorganised internal source generation components for improved modularity and code maintainability, separating parsing and code-emission logic into distinct modules.

Move Parser and Emitter from private nested classes within SourceGenerator
to internal top-level classes, enabling direct unit testing of each class
in isolation.

- Extract Parser to internal sealed class in Parser.cs
- Extract Emitter to internal sealed class in Emitter.cs
- Add InternalsVisibleTo for CompositeKey.SourceGeneration.UnitTests
- Exclude PolySharp ModuleInitializerAttribute polyfill to avoid type
  conflicts exposed by InternalsVisibleTo
- Resolve CompositeKeyAttribute assembly reference by name to avoid
  ambiguity between CompositeKey and CompositeKey.SourceGeneration
- Remove partial modifier from SourceGenerator class
@coderabbitai
Copy link

coderabbitai bot commented Feb 23, 2026

Walkthrough

This PR extracts nested Parser and Emitter classes from SourceGenerator into standalone internal top-level classes, enabling direct unit testing. Adds InternalsVisibleTo declaration and updates configuration to support test project visibility. Moves ~1,700 lines of code from nested to top-level scope without altering functionality.

Changes

Cohort / File(s) Summary
Test Infrastructure
src/CompositeKey.SourceGeneration.UnitTests/CompilationHelper.cs
Introduces preloaded CompositeKeyAssembly field and updates metadata reference to use the assembly location directly.
Project Configuration
src/CompositeKey.SourceGeneration/CompositeKey.SourceGeneration.csproj
Adds PolySharpExcludeGeneratedTypes property and InternalsVisibleTo declaration exposing internal members to unit tests.
Extracted Internal Classes
src/CompositeKey.SourceGeneration/Emitter.cs, src/CompositeKey.SourceGeneration/Parser.cs
New top-level internal sealed classes extracted from SourceGenerator nested classes. Emitter generates ToString, Parse, TryParse, and formatting logic; Parser validates types and extracts attribute data into GenerationSpec. Both classes enable direct unit testing.
Removed Nested Implementations
src/CompositeKey.SourceGeneration/SourceGenerator.Emitter.cs, src/CompositeKey.SourceGeneration/SourceGenerator.Parser.cs
Deletes nested Parser and Emitter class implementations (~1,705 lines combined) now extracted to top-level classes.
Updated Orchestrator
src/CompositeKey.SourceGeneration/SourceGenerator.cs
Converts class from partial to non-partial, adds CompositeKeyAttributeFullName constant field, and now references external top-level Parser and Emitter classes.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

enhancement

Poem

🐰 A Parser hops free, an Emitter takes flight,
No longer nested, now top-level and bright,
Internal and testing, together at last,
A refactor so clean, the die has been cast!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main structural refactor: extracting Parser and Emitter from private nested classes to internal top-level classes.
Linked Issues check ✅ Passed All coding requirements from issue #41 are met: Parser and Emitter extracted to internal top-level classes, InternalsVisibleTo added, SourceGenerator updated, and generated output remains identical.
Out of Scope Changes check ✅ Passed All changes are directly related to the refactoring objectives; no unrelated modifications or scope creep detected.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/extract-parser-emitter-to-internal-classes

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.

Copy link

@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.

Actionable comments posted: 1

🧹 Nitpick comments (4)
src/CompositeKey.SourceGeneration/Parser.cs (3)

28-36: ReportDiagnostic is public on an internal class — consider internal visibility.

Since Parser is internal sealed, public methods on it are effectively internal. However, marking it internal (or leaving it at the default for an internal class context) would be more intentional about the intended access scope.

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

In `@src/CompositeKey.SourceGeneration/Parser.cs` around lines 28 - 36, The
ReportDiagnostic method on the internal sealed Parser class is declared public;
change its visibility to internal to match the class scope and make intent
explicit. Update the ReportDiagnostic method declaration (method name:
ReportDiagnostic in Parser.cs) from public to internal, keeping the existing
parameters and body unchanged, so the method is only visible within the assembly
and consistent with the internal sealed Parser class.

577-624: ParseCompositeKeyAttributeValues iterates all attributes but does not break after finding a match.

The Debug.Assert at line 585 will fire in debug builds if there are somehow two CompositeKeyAttribute instances, but in release builds the loop will silently overwrite attributeValues. Since the attribute should have AllowMultiple = false, this is unlikely, but an early break after the match would make the intent clearer.

♻️ Suggested change
             if (SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, _knownTypeSymbols.CompositeKeyAttributeType))
+            {
                 attributeValues = TryExtractAttributeValues(attributeData);
+                break;
+            }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/CompositeKey.SourceGeneration/Parser.cs` around lines 577 - 624, The loop
in ParseCompositeKeyAttributeValues may continue after finding a matching
CompositeKeyAttribute and overwrite attributeValues in release builds; after
assigning attributeValues = TryExtractAttributeValues(attributeData) inside the
foreach where
SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass,
_knownTypeSymbols.CompositeKeyAttributeType) is true, add an immediate break to
exit the loop (preserving the existing Debug.Assert) so the first-found
attribute value is retained and no subsequent attributes can overwrite it.

15-15: Consider making CompositeKeyAttributeValues internal.

This record is only consumed by Parser (internal) and is within an analyzer assembly (shipped with PrivateAssets=all), so public visibility is broader than necessary. Changing to internal would reduce the public API surface of the analyzer assembly.

♻️ Suggested change
-public sealed record CompositeKeyAttributeValues(string TemplateString, char? PrimaryKeySeparator, bool InvariantCulture);
+internal sealed record CompositeKeyAttributeValues(string TemplateString, char? PrimaryKeySeparator, bool InvariantCulture);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/CompositeKey.SourceGeneration/Parser.cs` at line 15, The
CompositeKeyAttributeValues record is unnecessarily public; change its
declaration from public to internal so it becomes internal sealed record
CompositeKeyAttributeValues(string TemplateString, char? PrimaryKeySeparator,
bool InvariantCulture); and ensure any usages (e.g., in the internal Parser
type) still compile—adjust accessibility only on the CompositeKeyAttributeValues
declaration to reduce the analyzer assembly's public API surface.
src/CompositeKey.SourceGeneration/Emitter.cs (1)

374-381: Hardcoded stackalloc Range[128] limits repeating property collections to ~128 items.

When a collection has more than 128 items and uses the same separator as key delimiters, ReadOnlySpan<char>.Split will pack the remainder into the last range. This leads to a confusing FormatException rather than a clear error. While 128 is generous for typical usage, this is worth being aware of.

If this is an intentional design constraint, consider adding a comment documenting the limit. If it should be unbounded, consider an ArrayPool<Range>-based approach.

Also applies to: 560-568

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

In `@src/CompositeKey.SourceGeneration/Emitter.cs` around lines 374 - 381, The
stackalloc fixed buffer (stackalloc Range[128]) can overflow for large repeating
collections; replace it with a rented array from ArrayPool<Range> (e.g., var
rented = ArrayPool<Range>.Shared.Rent(capacity); try { Span<Range>
{partRangesVariableName} = rented; int {partCountVariableName} =
{inputName}.{method}(...); ... } finally {
ArrayPool<Range>.Shared.Return(rented); }) so the split can handle >128 parts;
apply the same change for the other occurrence (lines ~560-568) and ensure the
rented array is returned in a finally block and the Span is created from the
rented array before calling the split.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/CompositeKey.SourceGeneration/Parser.cs`:
- Around line 532-540: The loop that checks sequential enum member values uses
Convert.ToUInt64(members[i].Value) which throws OverflowException for negative
underlying enum values; change the check to guard against negatives by either
testing if Convert.ToInt64(members[i].Value) < 0 or wrapping the
Convert.ToUInt64 call in a try/catch that catches OverflowException and sets
isSequentialFromZero = false and breaks; also compare against (ulong)i instead
of (uint)i for type consistency. Locate the loop using members[i].Value and the
isSequentialFromZero variable in Parser.cs and apply the defensive check so the
source generator degrades gracefully for negative enum values.

---

Nitpick comments:
In `@src/CompositeKey.SourceGeneration/Emitter.cs`:
- Around line 374-381: The stackalloc fixed buffer (stackalloc Range[128]) can
overflow for large repeating collections; replace it with a rented array from
ArrayPool<Range> (e.g., var rented = ArrayPool<Range>.Shared.Rent(capacity); try
{ Span<Range> {partRangesVariableName} = rented; int {partCountVariableName} =
{inputName}.{method}(...); ... } finally {
ArrayPool<Range>.Shared.Return(rented); }) so the split can handle >128 parts;
apply the same change for the other occurrence (lines ~560-568) and ensure the
rented array is returned in a finally block and the Span is created from the
rented array before calling the split.

In `@src/CompositeKey.SourceGeneration/Parser.cs`:
- Around line 28-36: The ReportDiagnostic method on the internal sealed Parser
class is declared public; change its visibility to internal to match the class
scope and make intent explicit. Update the ReportDiagnostic method declaration
(method name: ReportDiagnostic in Parser.cs) from public to internal, keeping
the existing parameters and body unchanged, so the method is only visible within
the assembly and consistent with the internal sealed Parser class.
- Around line 577-624: The loop in ParseCompositeKeyAttributeValues may continue
after finding a matching CompositeKeyAttribute and overwrite attributeValues in
release builds; after assigning attributeValues =
TryExtractAttributeValues(attributeData) inside the foreach where
SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass,
_knownTypeSymbols.CompositeKeyAttributeType) is true, add an immediate break to
exit the loop (preserving the existing Debug.Assert) so the first-found
attribute value is retained and no subsequent attributes can overwrite it.
- Line 15: The CompositeKeyAttributeValues record is unnecessarily public;
change its declaration from public to internal so it becomes internal sealed
record CompositeKeyAttributeValues(string TemplateString, char?
PrimaryKeySeparator, bool InvariantCulture); and ensure any usages (e.g., in the
internal Parser type) still compile—adjust accessibility only on the
CompositeKeyAttributeValues declaration to reduce the analyzer assembly's public
API surface.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d43999a and cb83e25.

📒 Files selected for processing (7)
  • src/CompositeKey.SourceGeneration.UnitTests/CompilationHelper.cs
  • src/CompositeKey.SourceGeneration/CompositeKey.SourceGeneration.csproj
  • src/CompositeKey.SourceGeneration/Emitter.cs
  • src/CompositeKey.SourceGeneration/Parser.cs
  • src/CompositeKey.SourceGeneration/SourceGenerator.Emitter.cs
  • src/CompositeKey.SourceGeneration/SourceGenerator.Parser.cs
  • src/CompositeKey.SourceGeneration/SourceGenerator.cs
💤 Files with no reviewable changes (2)
  • src/CompositeKey.SourceGeneration/SourceGenerator.Emitter.cs
  • src/CompositeKey.SourceGeneration/SourceGenerator.Parser.cs

@DrBarnabus DrBarnabus merged commit fcb18e9 into main Feb 23, 2026
2 checks passed
@DrBarnabus DrBarnabus deleted the refactor/extract-parser-emitter-to-internal-classes branch February 23, 2026 21:19
@codecov
Copy link

codecov bot commented Feb 23, 2026

Codecov Report

❌ Patch coverage is 85.64257% with 143 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.66%. Comparing base (d43999a) to head (cb83e25).
⚠️ Report is 2 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/CompositeKey.SourceGeneration/Emitter.cs 87.59% 59 Missing and 24 partials ⚠️
src/CompositeKey.SourceGeneration/Parser.cs 81.65% 34 Missing and 26 partials ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main      #56   +/-   ##
=======================================
  Coverage   87.66%   87.66%           
=======================================
  Files          35       35           
  Lines        2099     2099           
  Branches      344      344           
=======================================
  Hits         1840     1840           
  Misses        157      157           
  Partials      102      102           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

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.

refactor: Extract Parser and Emitter to internal top-level classes

1 participant