Skip to main content

cmakefmt/config/
mod.rs

1// SPDX-FileCopyrightText: Copyright 2026 Puneet Matharu
2//
3// SPDX-License-Identifier: MIT OR Apache-2.0
4
5//! Runtime formatter configuration.
6//!
7//! [`Config`] is the fully resolved in-memory configuration used by the
8//! formatter. It is built from defaults, user config files
9//! (`.cmakefmt.yaml`, `.cmakefmt.yml`, or `.cmakefmt.toml`), and CLI
10//! overrides.
11
12#[cfg(all(not(target_arch = "wasm32"), feature = "cli"))]
13pub mod file;
14#[cfg(all(not(target_arch = "wasm32"), feature = "cli"))]
15mod legacy;
16/// Render a commented starter config template.
17#[cfg(all(not(target_arch = "wasm32"), feature = "cli"))]
18pub use file::{
19    default_config_template, default_config_template_for, render_effective_config, DumpConfigFormat,
20};
21#[cfg(all(not(target_arch = "wasm32"), feature = "cli"))]
22pub use legacy::convert_legacy_config_files;
23
24use std::collections::HashMap;
25
26use serde::{Deserialize, Serialize};
27
28/// How to normalise command/keyword casing.
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
30#[cfg_attr(feature = "cli", derive(clap::ValueEnum))]
31#[serde(rename_all = "lowercase")]
32pub enum CaseStyle {
33    /// Force lowercase output.
34    Lower,
35    /// Force uppercase output.
36    #[default]
37    Upper,
38    /// Preserve the original source casing.
39    Unchanged,
40}
41
42/// Output line-ending style.
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
44#[cfg_attr(feature = "cli", derive(clap::ValueEnum))]
45#[serde(rename_all = "lowercase")]
46pub enum LineEnding {
47    /// Unix-style LF (`\n`). The default.
48    #[default]
49    Unix,
50    /// Windows-style CRLF (`\r\n`).
51    Windows,
52    /// Auto-detect the line ending from the input source.
53    Auto,
54}
55
56/// How to handle fractional tab indentation when [`Config::use_tabchars`] is
57/// `true`.
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
59#[cfg_attr(feature = "cli", derive(clap::ValueEnum))]
60#[serde(rename_all = "kebab-case")]
61pub enum FractionalTabPolicy {
62    /// Leave fractional spaces as-is (utf-8 0x20). The default.
63    #[default]
64    UseSpace,
65    /// Round fractional indentation up to the next full tab stop (utf-8 0x09).
66    RoundUp,
67}
68
69/// How to align the dangling closing paren.
70///
71/// Only takes effect when [`Config::dangle_parens`] is `true`.
72/// Controls where `)` is placed when a call wraps onto multiple lines:
73///
74/// ```cmake
75/// # Prefix / Close — `)` at the command-name column (tracks block depth):
76/// target_link_libraries(
77///   mylib PUBLIC dep1
78/// )
79///
80/// # Open — `)` at the opening-paren column:
81/// target_link_libraries(
82///   mylib PUBLIC dep1
83///                      )
84/// ```
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
86#[serde(rename_all = "lowercase")]
87pub enum DangleAlign {
88    /// Align with the start of the command name.
89    #[default]
90    Prefix,
91    /// Align with the opening paren column.
92    Open,
93    /// No extra indent (flush with current indent level).
94    Close,
95}
96
97/// Full formatter configuration.
98///
99/// Construct [`Config::default`] and set fields as needed before passing it to
100/// [`format_source`](crate::format_source) or related functions.
101///
102/// ```
103/// use cmakefmt::{Config, CaseStyle, DangleAlign};
104///
105/// let config = Config {
106///     line_width: 100,
107///     command_case: CaseStyle::Lower,
108///     dangle_parens: true,
109///     dangle_align: DangleAlign::Open,
110///     ..Config::default()
111/// };
112/// ```
113///
114/// # Defaults
115///
116/// | Field | Default |
117/// |-------|---------|
118/// | `line_width` | `80` |
119/// | `tab_size` | `2` |
120/// | `use_tabchars` | `false` |
121/// | `max_empty_lines` | `1` |
122/// | `command_case` | [`CaseStyle::Lower`] |
123/// | `keyword_case` | [`CaseStyle::Upper`] |
124/// | `dangle_parens` | `false` |
125/// | `dangle_align` | [`DangleAlign::Prefix`] |
126/// | `enable_markup` | `true` |
127/// | `reflow_comments` | `false` |
128/// | `first_comment_is_literal` | `true` |
129#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
130#[serde(default)]
131pub struct Config {
132    // ── Kill-switch ─────────────────────────────────────────────────────
133    /// When `true`, skip all formatting and return the source unchanged.
134    pub disable: bool,
135
136    // ── Line endings ─────────────────────────────────────────────────────
137    /// Output line-ending style.
138    pub line_ending: LineEnding,
139
140    // ── Layout ──────────────────────────────────────────────────────────
141    /// Maximum rendered line width before wrapping is attempted.
142    pub line_width: usize,
143    /// Number of spaces that make up one indentation level when
144    /// [`Self::use_tabchars`] is `false`.
145    pub tab_size: usize,
146    /// Emit tab characters for indentation instead of spaces.
147    pub use_tabchars: bool,
148    /// How to handle fractional indentation when [`Self::use_tabchars`] is
149    /// `true`.
150    pub fractional_tab_policy: FractionalTabPolicy,
151    /// Maximum number of consecutive empty lines to preserve.
152    pub max_empty_lines: usize,
153    /// Maximum number of wrapped lines tolerated before switching to a more
154    /// vertical layout.
155    pub max_lines_hwrap: usize,
156    /// Maximum number of positional arguments to keep in a hanging-wrap layout
157    /// before going vertical.
158    pub max_pargs_hwrap: usize,
159    /// Maximum number of keyword/flag subgroups to keep in a horizontal wrap.
160    pub max_subgroups_hwrap: usize,
161    /// Maximum rows a hanging-wrap positional group may consume before the
162    /// layout is rejected and nesting is forced.
163    pub max_rows_cmdline: usize,
164    /// Command names (lowercase) that must always use vertical layout,
165    /// regardless of line width.
166    pub always_wrap: Vec<String>,
167    /// Return an error when any formatted output line exceeds
168    /// [`Self::line_width`].
169    pub require_valid_layout: bool,
170
171    // ── Parenthesis style ───────────────────────────────────────────────
172    /// Place the closing `)` on its own line when a call wraps.
173    pub dangle_parens: bool,
174    /// Alignment strategy for a dangling closing `)`.
175    pub dangle_align: DangleAlign,
176    /// Lower bound used by layout heuristics when deciding whether a command
177    /// name is short enough to prefer one style over another.
178    pub min_prefix_chars: usize,
179    /// Upper bound used by layout heuristics when deciding whether a command
180    /// name is long enough to prefer one style over another.
181    pub max_prefix_chars: usize,
182    /// Insert a space before `(` for control-flow commands such as `if`.
183    pub separate_ctrl_name_with_space: bool,
184    /// Insert a space before `(` for `function`/`macro` definitions.
185    pub separate_fn_name_with_space: bool,
186
187    // ── Casing ──────────────────────────────────────────────────────────
188    /// Output casing policy for command names.
189    pub command_case: CaseStyle,
190    /// Output casing policy for recognized keywords and flags.
191    pub keyword_case: CaseStyle,
192
193    // ── Comment markup ──────────────────────────────────────────────────
194    /// Enable markup-aware comment handling.
195    pub enable_markup: bool,
196    /// Reflow plain line comments to fit within the configured width.
197    pub reflow_comments: bool,
198    /// Preserve the first comment block in a file literally.
199    pub first_comment_is_literal: bool,
200    /// Regex for comments that should never be reflowed.
201    pub literal_comment_pattern: String,
202    /// Preferred bullet character when normalizing list markup.
203    pub bullet_char: String,
204    /// Preferred enumeration punctuation when normalizing numbered list markup.
205    pub enum_char: String,
206    /// Regex describing fenced literal comment blocks.
207    pub fence_pattern: String,
208    /// Regex describing ruler-style comments.
209    pub ruler_pattern: String,
210    /// Minimum ruler length before a `#-----` style line is treated as a ruler.
211    pub hashruler_min_length: usize,
212    /// Normalize ruler comments when markup handling is enabled.
213    pub canonicalize_hashrulers: bool,
214    /// Regex pattern that marks an inline comment as explicitly trailing its
215    /// preceding argument. Matching comments are rendered on the same line as
216    /// the preceding token rather than on their own line.
217    pub explicit_trailing_pattern: String,
218
219    // ── Per-command overrides ────────────────────────────────────────────
220    /// Per-command configuration overrides keyed by lowercase command name.
221    pub per_command_overrides: HashMap<String, PerCommandConfig>,
222}
223
224/// Per-command overrides. All fields are optional — only specified fields
225/// override the global config for that command.
226#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
227#[serde(deny_unknown_fields)]
228pub struct PerCommandConfig {
229    /// Override the command casing rule for this command only.
230    pub command_case: Option<CaseStyle>,
231    /// Override the keyword casing rule for this command only.
232    pub keyword_case: Option<CaseStyle>,
233    /// Override the line width for this command only.
234    pub line_width: Option<usize>,
235    /// Override the indentation width for this command only.
236    pub tab_size: Option<usize>,
237    /// Override dangling paren placement for this command only.
238    pub dangle_parens: Option<bool>,
239    /// Override dangling paren alignment for this command only.
240    pub dangle_align: Option<DangleAlign>,
241    /// Override the hanging-wrap positional argument threshold for this
242    /// command only.
243    #[serde(rename = "max_hanging_wrap_positional_args")]
244    pub max_pargs_hwrap: Option<usize>,
245    /// Override the hanging-wrap subgroup threshold for this command only.
246    #[serde(rename = "max_hanging_wrap_groups")]
247    pub max_subgroups_hwrap: Option<usize>,
248}
249
250impl Default for Config {
251    fn default() -> Self {
252        Self {
253            disable: false,
254            line_ending: LineEnding::Unix,
255            line_width: 80,
256            tab_size: 2,
257            use_tabchars: false,
258            fractional_tab_policy: FractionalTabPolicy::UseSpace,
259            max_empty_lines: 1,
260            max_lines_hwrap: 2,
261            max_pargs_hwrap: 6,
262            max_subgroups_hwrap: 2,
263            max_rows_cmdline: 2,
264            always_wrap: Vec::new(),
265            require_valid_layout: false,
266            dangle_parens: false,
267            dangle_align: DangleAlign::Prefix,
268            min_prefix_chars: 4,
269            max_prefix_chars: 10,
270            separate_ctrl_name_with_space: false,
271            separate_fn_name_with_space: false,
272            command_case: CaseStyle::Lower,
273            keyword_case: CaseStyle::Upper,
274            enable_markup: true,
275            reflow_comments: false,
276            first_comment_is_literal: true,
277            literal_comment_pattern: String::new(),
278            bullet_char: "*".to_string(),
279            enum_char: ".".to_string(),
280            fence_pattern: r"^\s*[`~]{3}[^`\n]*$".to_string(),
281            ruler_pattern: r"^[^\w\s]{3}.*[^\w\s]{3}$".to_string(),
282            hashruler_min_length: 10,
283            canonicalize_hashrulers: true,
284            explicit_trailing_pattern: "#<".to_string(),
285            per_command_overrides: HashMap::new(),
286        }
287    }
288}
289
290/// CMake control-flow commands that get `separate_ctrl_name_with_space`.
291const CONTROL_FLOW_COMMANDS: &[&str] = &[
292    "if",
293    "elseif",
294    "else",
295    "endif",
296    "foreach",
297    "endforeach",
298    "while",
299    "endwhile",
300    "break",
301    "continue",
302    "return",
303    "block",
304    "endblock",
305];
306
307/// CMake function/macro definition commands that get
308/// `separate_fn_name_with_space`.
309const FN_DEFINITION_COMMANDS: &[&str] = &["function", "endfunction", "macro", "endmacro"];
310
311impl Config {
312    /// Returns a `Config` with any per-command overrides applied for the
313    /// given command name, plus the appropriate space-before-paren setting.
314    pub fn for_command(&self, command_name: &str) -> CommandConfig<'_> {
315        let lower = command_name.to_ascii_lowercase();
316        let per_cmd = self.per_command_overrides.get(&lower);
317
318        let space_before_paren = if CONTROL_FLOW_COMMANDS.contains(&lower.as_str()) {
319            self.separate_ctrl_name_with_space
320        } else if FN_DEFINITION_COMMANDS.contains(&lower.as_str()) {
321            self.separate_fn_name_with_space
322        } else {
323            false
324        };
325
326        CommandConfig {
327            global: self,
328            per_cmd,
329            space_before_paren,
330        }
331    }
332
333    /// Apply the command_case rule to a command name.
334    pub fn apply_command_case(&self, name: &str) -> String {
335        apply_case(self.command_case, name)
336    }
337
338    /// Apply the keyword_case rule to a keyword token.
339    pub fn apply_keyword_case(&self, keyword: &str) -> String {
340        apply_case(self.keyword_case, keyword)
341    }
342
343    /// The indentation string (spaces or tab).
344    pub fn indent_str(&self) -> String {
345        if self.use_tabchars {
346            "\t".to_string()
347        } else {
348            " ".repeat(self.tab_size)
349        }
350    }
351}
352
353/// A resolved config for formatting a specific command, with per-command
354/// overrides already applied.
355#[derive(Debug)]
356pub struct CommandConfig<'a> {
357    /// The global configuration before per-command overrides are applied.
358    pub global: &'a Config,
359    per_cmd: Option<&'a PerCommandConfig>,
360    /// Whether this command should render a space before `(`.
361    pub space_before_paren: bool,
362}
363
364impl CommandConfig<'_> {
365    /// Effective line width for the current command.
366    pub fn line_width(&self) -> usize {
367        self.per_cmd
368            .and_then(|p| p.line_width)
369            .unwrap_or(self.global.line_width)
370    }
371
372    /// Effective indentation width for the current command.
373    pub fn tab_size(&self) -> usize {
374        self.per_cmd
375            .and_then(|p| p.tab_size)
376            .unwrap_or(self.global.tab_size)
377    }
378
379    /// Effective dangling-paren setting for the current command.
380    pub fn dangle_parens(&self) -> bool {
381        self.per_cmd
382            .and_then(|p| p.dangle_parens)
383            .unwrap_or(self.global.dangle_parens)
384    }
385
386    /// Effective dangling-paren alignment for the current command.
387    pub fn dangle_align(&self) -> DangleAlign {
388        self.per_cmd
389            .and_then(|p| p.dangle_align)
390            .unwrap_or(self.global.dangle_align)
391    }
392
393    /// Effective command casing rule for the current command.
394    pub fn command_case(&self) -> CaseStyle {
395        self.per_cmd
396            .and_then(|p| p.command_case)
397            .unwrap_or(self.global.command_case)
398    }
399
400    /// Effective keyword casing rule for the current command.
401    pub fn keyword_case(&self) -> CaseStyle {
402        self.per_cmd
403            .and_then(|p| p.keyword_case)
404            .unwrap_or(self.global.keyword_case)
405    }
406
407    /// Effective hanging-wrap positional argument threshold for the current
408    /// command.
409    pub fn max_pargs_hwrap(&self) -> usize {
410        self.per_cmd
411            .and_then(|p| p.max_pargs_hwrap)
412            .unwrap_or(self.global.max_pargs_hwrap)
413    }
414
415    /// Effective hanging-wrap subgroup threshold for the current command.
416    pub fn max_subgroups_hwrap(&self) -> usize {
417        self.per_cmd
418            .and_then(|p| p.max_subgroups_hwrap)
419            .unwrap_or(self.global.max_subgroups_hwrap)
420    }
421
422    /// Effective indentation unit for the current command.
423    pub fn indent_str(&self) -> String {
424        if self.global.use_tabchars {
425            "\t".to_string()
426        } else {
427            " ".repeat(self.tab_size())
428        }
429    }
430}
431
432fn apply_case(style: CaseStyle, s: &str) -> String {
433    match style {
434        CaseStyle::Lower => s.to_ascii_lowercase(),
435        CaseStyle::Upper => s.to_ascii_uppercase(),
436        CaseStyle::Unchanged => s.to_string(),
437    }
438}