Skip to content

C#: Add .editorconfig-based formatting style detection for RoslynFormatter#7194

Merged
knutwannheden merged 16 commits intomainfrom
c-style-detection-for-roslynformatter
Mar 29, 2026
Merged

C#: Add .editorconfig-based formatting style detection for RoslynFormatter#7194
knutwannheden merged 16 commits intomainfrom
c-style-detection-for-roslynformatter

Conversation

@knutwannheden
Copy link
Copy Markdown
Contributor

@knutwannheden knutwannheden commented Mar 29, 2026

Motivation

The C# RoslynFormatter hardcoded WrappingPreserveSingleLine=false and WrappingKeepStatementsOnSingleLine=false in BuildOptions, but didn't set any NewLinesForBraces* options. Roslyn's AdhocWorkspace defaults 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 a NamedStyles marker).

Summary

  • New CSharpFormatStyle (C# + Java with RPC codecs) — extends NamedStyles in csharp.style package, following the PrettierStyle pattern 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.
  • New EditorConfigResolver — parses .editorconfig files hierarchically from file directory up to project root, layering child over parent, stopping at root = 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.
  • SolutionParser creates the resolver and attaches the CSharpFormatStyle marker to each CompilationUnit during parsing
  • RoslynFormatter reads the marker from the CU instead of detecting at format time, and wires all brace/keyword options into CSharpFormattingOptions
  • Deleted FormatStyle — the old source-text heuristic class, superseded by EditorConfigResolver

Test plan

  • 19 EditorConfigResolverTests covering: no .editorconfig (Roslyn defaults), tabs/spaces detection, indent size, end_of_line, all/none/comma-list for csharp_new_line_before_open_brace, else/catch/finally options, wrapping options, hierarchical layering, partial override, root = true stops search, caching (same instance for same directory), wildcard vs *.cs section precedence, comments/blank lines, indent_size = tab edge case
  • All 22 existing AutoFormatTests pass (3 dead FormatStyle tests removed)
  • Java side compiles (gw :rewrite-csharp:compileJava)
  • License headers verified (gw :rewrite-csharp:licenseFormatCsharp)

…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
@knutwannheden knutwannheden changed the title C#: Add .editorconfig-based formatting style detection for RoslynFormatter C#: Add .editorconfig-based formatting style detection for RoslynFormatter Mar 29, 2026
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).
@knutwannheden knutwannheden merged commit c27eebc into main Mar 29, 2026
1 check passed
@knutwannheden knutwannheden deleted the c-style-detection-for-roslynformatter branch March 29, 2026 16:28
@github-project-automation github-project-automation Bot moved this from In Progress to Done in OpenRewrite Mar 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

1 participant