C#: Add .editorconfig-based formatting style detection for RoslynFormatter#7194
Merged
knutwannheden merged 16 commits intomainfrom Mar 29, 2026
Merged
C#: Add .editorconfig-based formatting style detection for RoslynFormatter#7194knutwannheden merged 16 commits intomainfrom
.editorconfig-based formatting style detection for RoslynFormatter#7194knutwannheden merged 16 commits intomainfrom
Conversation
…atter Replace the runtime source-text heuristic (FormatStyle.DetectStyle) with a proper .editorconfig-based style resolver. A new CSharpFormatStyle marker is attached to each CompilationUnit during parsing and flows through RPC to the Java side. - Add CSharpFormatStyle marker (C# + Java) with RPC codecs carrying all Roslyn formatting options: tabs/spaces, indent size, newline style, 10 brace placement options, 3 keyword newline options, 2 wrapping options - Add EditorConfigResolver that walks .editorconfig files hierarchically from file directory up to project root, layering child over parent, stopping at root=true. Caches resolved styles per directory. - SolutionParser attaches the resolved CSharpFormatStyle to each CU - RoslynFormatter reads the marker instead of detecting at format time and wires all brace/keyword options into CSharpFormattingOptions - Delete dead FormatStyle class superseded by EditorConfigResolver
Follow the established convention where style markers extend NamedStyles (like PrettierStyle, Autodetect) and live in a .style package rather than .marker. The CSharpFormatStyle now extends NamedStyles and stores its configuration directly (empty styles collection), matching the PrettierStyle pattern for formatters that delegate to a single external tool. - Move Java class from csharp.marker to csharp.style package - Extend NamedStyles instead of implementing Marker directly - Update RPC type name to org.openrewrite.csharp.style.CSharpFormatStyle - Add style package convention to RpcReceiveQueue type resolution
.editorconfig-based formatting style detection for RoslynFormatter
Real-world .editorconfig files use Roslyn severity suffixes like csharp_new_line_before_open_brace = all:error or csharp_new_line_before_else = true:suggestion. Strip the :suffix before processing so these values are correctly recognized.
Expand CSharpFormatStyle to carry every option that Roslyn's CSharpFormattingOptions exposes, and wire them all through BuildOptions. Added options: - Indentation: IndentBlock, IndentBraces, IndentSwitchCaseSection, IndentSwitchCaseSectionWhenBlock, IndentSwitchSection, LabelPositioning - New lines: NewLineForClausesInQuery, NewLineForMembersInAnonymousTypes, NewLineForMembersInObjectInit - Spacing: all 24 Space* options including SpacingAroundBinaryOperator (enum mapped as int: 0=before_and_after, 1=ignore, 2=none) EditorConfigResolver now parses .editorconfig keys for all of these, including csharp_indent_labels (flush_left/one_less_than_current/no_change), csharp_space_around_binary_operators (before_and_after/ignore/none), and csharp_space_around_declaration_statements (ignore/false).
Replace 47 individual boolean fields with bit-packed long storage. Public API is unchanged — all boolean properties remain as computed accessors (e.g., style.SpaceAfterCast reads bit 22 from the packed long). RPC now sends 6 fields instead of 52. Bit positions are assigned in declaration order and are append-only to maintain wire compatibility. Both C# and Java sides use matching bit position constants.
The packed long has 27 of 47 bits set by default (Allman style, standard spacing). Making this an explicit constant (0x600033BFFFFAL) documents the defaults, simplifies the Default instance construction, and protects against accidental zero-initialization.
The OptionSet built from 51 WithChangedOption calls is now lazily cached as a transient field on the immutable CSharpFormatStyle marker. Since markers are shared across files in the same directory, the cached OptionSet is reused for all format calls on those files. The field is volatile for safe publication and excluded from RPC serialization and equality — it's rebuilt on demand after deserialization.
Two new tests confirm that CSharpFormatStyle options flow through to Roslyn's formatter correctly: - K&R brace style (NewLinesForBracesInTypes/Methods=false) produces braces on same line as declaration - SpaceAfterCast=true inserts a space after cast expressions
All boolean, int, and string parameters in the convenience constructor now have default values matching Roslyn/VS defaults. Callers only need to specify the values they want to override, e.g.: new CSharpFormatStyle(id, newLinesForBracesInTypes: false)
When a subtree has a structural mismatch (e.g., a recipe constructed a J.Identifier where the parser would produce J.Primitive), the reconciler now skips that subtree — keeping its original whitespace — and continues reconciling the rest of the tree. Previously, any mismatch aborted the entire reconciliation and returned the original tree unchanged. - StructureMismatch now records the mismatch count but does not abort - Remove all early-exit guards that checked _compatible flag - Add MismatchCount property and MaxMismatches limit (throws when exceeded, intended for test assertions) - RoslynFormatter no longer discards reconciled results on mismatch The skipped test (SkipsMismatchedSubtreeAndReconcilesSurroundingCode) demonstrates the intended behavior but needs investigation — the result propagation doesn't update surrounding whitespace as expected.
The last remaining early-exit guard (inside VisitList's for loop)
was preventing siblings from being reconciled after a mismatch in
an earlier sibling. Removing it allows the reconciler to process
all list elements, skipping only the subtrees that actually mismatch.
The new test verifies this: when a recipe replaces J.Primitive("int")
with J.Identifier("int") on the first method's return type, the
second method and its body still get proper indentation from Roslyn.
Introduce Validations as a centralized way to enable/disable test
invariant checks in RewriteRun, following the Java-side TypeValidation
pattern. Tests configure it via RecipeSpec:
spec.SetValidations(new Validations { WhitespaceInSpaces = false })
Current flags:
- WhitespaceInSpaces: validate Space fields contain only whitespace
- PrintEqualsInput: validate round-trip fidelity
- PrintIdempotence: validate re-parse produces identical output
- MaxWhitespaceMismatches: limit for reconciler structural mismatches
All enabled by default. Validations.None disables everything.
RewriteRun now sets WhitespaceReconciler.DefaultMaxMismatches from the Validations config before running recipes and restores it after. The static field defaults to 0 which maps to int.MaxValue (unlimited) in production. Tests get the Validations.All default of 5.
Replace MaxWhitespaceMismatches (int) with AllowWhitespaceMismatches
(bool). The reconciler now collects mismatches during the full walk
and throws after completion with a report of the first 5 — no early
abort. Tests get ThrowOnMismatch=true by default (via Validations.All).
Production gets ThrowOnMismatch=false (static default).
Tests that expect mismatches disable throwing:
spec.SetValidations(new Validations { AllowWhitespaceMismatches = true })
Better describes what's being validated: structural compatibility between recipe output and Roslyn-formatted output during reconciliation.
Resolve conflicts in WhitespaceReconciler by taking main's improved structure (constructor-based throwOnMismatch, path tracking, MismatchEntry records, WhitespaceReconcileMismatchException) while preserving this branch's skip-past-mismatch behavior (removed early-exit guards).
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.
Motivation
The C#
RoslynFormatterhardcodedWrappingPreserveSingleLine=falseandWrappingKeepStatementsOnSingleLine=falseinBuildOptions, but didn't set anyNewLinesForBraces*options. Roslyn'sAdhocWorkspacedefaults don't match Visual Studio/IDE defaults, so synthesized nodes got inconsistent formatting. Additionally, style detection was done at format time by scanning the printed source text — a heuristic that couldn't detect brace placement style and ran on every format call.This PR replaces the runtime heuristic with proper
.editorconfig-based style resolution, following the same pattern as the JavaScript/TypeScript module (which resolves Prettier config during parsing and attaches it as aNamedStylesmarker).Summary
CSharpFormatStyle(C# + Java with RPC codecs) — extendsNamedStylesincsharp.stylepackage, following thePrettierStylepattern for formatters that delegate to a single external tool. Carries all Roslyn formatting options: tabs/spaces, indent size, tab size, newline style, 10 brace placement options, 3 keyword newline options, 2 wrapping options.EditorConfigResolver— parses.editorconfigfiles hierarchically from file directory up to project root, layering child over parent, stopping atroot = true. Supports[*]and[*.cs]sections,csharp_new_line_before_open_brace = all|none|comma,list,indent_size = tab, and all standard EditorConfig keys. Caches resolved styles per directory so files in the same directory share one marker instance.SolutionParsercreates the resolver and attaches theCSharpFormatStylemarker to eachCompilationUnitduring parsingRoslynFormatterreads the marker from the CU instead of detecting at format time, and wires all brace/keyword options intoCSharpFormattingOptionsFormatStyle— the old source-text heuristic class, superseded byEditorConfigResolverTest plan
EditorConfigResolverTestscovering: no .editorconfig (Roslyn defaults), tabs/spaces detection, indent size, end_of_line,all/none/comma-list forcsharp_new_line_before_open_brace, else/catch/finally options, wrapping options, hierarchical layering, partial override,root = truestops search, caching (same instance for same directory), wildcard vs*.cssection precedence, comments/blank lines,indent_size = tabedge caseAutoFormatTestspass (3 deadFormatStyletests removed)gw :rewrite-csharp:compileJava)gw :rewrite-csharp:licenseFormatCsharp)