Formatter and Emitters
This page describes the formatting architecture in BSharp, implemented in the bsharp_syntax crate.
Overview
The formatter is an AST-driven emitter that produces the final C# text directly. There is no post-processing pass (no normalize_text): the output is exactly what emitters write.
- Core types:
FormatterFormatOptions
- Emission is instrumentable via a JSONL trace for debugging and profiling.
FormatOptions
#![allow(unused)] fn main() { pub struct FormatOptions { pub indent_width: usize, // default: 4 spaces pub newline: &'static str, // "\n" or "\r\n" pub max_consecutive_blank_lines: u8, // default: 1 pub blank_line_between_members: bool, // default: true pub ensure_final_newline: bool, // default: true (emit one final newline if any content) pub trim_trailing_whitespace: bool, // default: true pub instrument_emission: bool, // default: false pub trace_file: Option<std::path::PathBuf>, // optional JSONL output pub current_file: Option<std::path::PathBuf>, // helpful in messages } }
- Newline mode is controlled by CLI
--newline-modeor defaults to LF. - Emission tracing can be toggled via CLI
--emit-traceorBSHARP_EMIT_TRACE=1.
Brace Style and Spacing Policy
-
Brace style: All containers and headers use Allman style
- Header ends the line (e.g.,
namespace X,class C,void M()) - Next line is an opening
{, indented body, then closing}on its own line.
- Header ends the line (e.g.,
-
Spacing is centralized in simple policy helpers (see
src/bsharp_syntax/src/emitters/policy.rs):between_header_and_body_of_file→ blank line between file header (e.g., file-scoped ns) and bodyafter_file_scoped_namespace_header→ blank line afternamespace X.Y;between_using_blocks_and_declarations→ blank line after using block before first declarationbetween_top_level_declarations→ single separator newline between top-level declarationsbetween_members→ single separator newline between adjacent type membersbetween_block_items→ optional extra newline inside a block when a control-flow block (if/for/while/do/switch/inner block) is followed by a declaration
Notes:
- Policies are invoked from emitters; emitters themselves keep logic minimal and do not hardcode extra blank lines.
- Interfaces, classes, structs, and records call
between_membersbetween members; the booleanblank_line_between_memberstoggles this globally.
End-of-file Newline
- The
CompilationUnitemitter ensures at most one final newline at EOF. - There are no per-statement trailing newlines at the root; separation is handled by policy functions.
Usage
#![allow(unused)] fn main() { use bsharp_syntax::{Formatter, FormatOptions}; let mut opts = FormatOptions::default(); opts.newline = "\n"; opts.max_consecutive_blank_lines = 1; opts.blank_line_between_members = true; opts.trim_trailing_whitespace = true; let fmt = Formatter::new(opts); let output = fmt.format_compilation_unit(&cu)?; // cu: CompilationUnit }
Emission Trace (JSONL)
When instrumentation is enabled, the formatter emits a stream of JSON objects describing emission steps.
- CLI integration:
--emit-traceto enable--emit-trace-file <FILE>to write to a file (stdout by default)- Env var
BSHARP_EMIT_TRACE=1acts as a default toggle
The trace can be useful to:
- Diagnose spacing/blank line decisions (look for
action: "policy"with names likebetween_members,between_top_level_declarations,between_block_items) - Identify costly emission paths
- Reproduce formatting anomalies
Typical actions include: enter_node, open_brace, close_brace, newline, space, token, and policy.
Integration with CLI
- See
bsharp formatindocs/cli/format.mdfor options mapping toFormatOptions. - Files that fail to parse are skipped; a summary is printed.
- With
--write falseon a single file input, the formatted output is printed to stdout.
Design Notes
- Emitters are AST-driven to preserve structure while normalizing whitespace and layout based on policies.
- The formatter avoids changing semantics and focuses on consistent style.
- Options default to safe, conservative values and can be tuned via CLI.