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
12pub mod file;
13mod legacy;
14/// Render a commented starter config template.
15pub use file::{
16    default_config_template, default_config_template_for, render_effective_config, DumpConfigFormat,
17};
18pub use legacy::convert_legacy_config_files;
19
20use std::collections::HashMap;
21
22use serde::{Deserialize, Serialize};
23
24/// How to normalise command/keyword casing.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, clap::ValueEnum)]
26#[serde(rename_all = "lowercase")]
27pub enum CaseStyle {
28    /// Force lowercase output.
29    Lower,
30    /// Force uppercase output.
31    #[default]
32    Upper,
33    /// Preserve the original source casing.
34    Unchanged,
35}
36
37/// How to align the dangling closing paren.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
39#[serde(rename_all = "lowercase")]
40pub enum DangleAlign {
41    /// Align with the start of the command name.
42    #[default]
43    Prefix,
44    /// Align with the opening paren column.
45    Open,
46    /// No extra indent (flush with current indent level).
47    Close,
48}
49
50/// Full formatter configuration.
51///
52/// This struct is used at runtime. It is populated from defaults, supported
53/// user config files (YAML or TOML), and CLI flag overrides (highest wins).
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct Config {
56    // ── Layout ──────────────────────────────────────────────────────────
57    /// Maximum rendered line width before wrapping is attempted.
58    pub line_width: usize,
59    /// Number of spaces that make up one indentation level when
60    /// [`Self::use_tabchars`] is `false`.
61    pub tab_size: usize,
62    /// Emit tab characters for indentation instead of spaces.
63    pub use_tabchars: bool,
64    /// Maximum number of consecutive empty lines to preserve.
65    pub max_empty_lines: usize,
66    /// Maximum number of wrapped lines tolerated before switching to a more
67    /// vertical layout.
68    pub max_lines_hwrap: usize,
69    /// Maximum number of positional arguments to keep in a hanging-wrap layout
70    /// before going vertical.
71    pub max_pargs_hwrap: usize,
72    /// Maximum number of keyword/flag subgroups to keep in a horizontal wrap.
73    pub max_subgroups_hwrap: usize,
74
75    // ── Parenthesis style ───────────────────────────────────────────────
76    /// Place the closing `)` on its own line when a call wraps.
77    pub dangle_parens: bool,
78    /// Alignment strategy for a dangling closing `)`.
79    pub dangle_align: DangleAlign,
80    /// Lower bound used by layout heuristics when deciding whether a command
81    /// name is short enough to prefer one style over another.
82    pub min_prefix_chars: usize,
83    /// Upper bound used by layout heuristics when deciding whether a command
84    /// name is long enough to prefer one style over another.
85    pub max_prefix_chars: usize,
86    /// Insert a space before `(` for control-flow commands such as `if`.
87    pub separate_ctrl_name_with_space: bool,
88    /// Insert a space before `(` for `function`/`macro` definitions.
89    pub separate_fn_name_with_space: bool,
90
91    // ── Casing ──────────────────────────────────────────────────────────
92    /// Output casing policy for command names.
93    pub command_case: CaseStyle,
94    /// Output casing policy for recognized keywords and flags.
95    pub keyword_case: CaseStyle,
96
97    // ── Comment markup ──────────────────────────────────────────────────
98    /// Enable markup-aware comment handling.
99    pub enable_markup: bool,
100    /// Reflow plain line comments to fit within the configured width.
101    pub reflow_comments: bool,
102    /// Preserve the first comment block in a file literally.
103    pub first_comment_is_literal: bool,
104    /// Regex for comments that should never be reflowed.
105    pub literal_comment_pattern: String,
106    /// Preferred bullet character when normalizing list markup.
107    pub bullet_char: String,
108    /// Preferred enumeration punctuation when normalizing numbered list markup.
109    pub enum_char: String,
110    /// Regex describing fenced literal comment blocks.
111    pub fence_pattern: String,
112    /// Regex describing ruler-style comments.
113    pub ruler_pattern: String,
114    /// Minimum ruler length before a `#-----` style line is treated as a ruler.
115    pub hashruler_min_length: usize,
116    /// Normalize ruler comments when markup handling is enabled.
117    pub canonicalize_hashrulers: bool,
118
119    // ── Per-command overrides ────────────────────────────────────────────
120    /// Per-command configuration overrides keyed by lowercase command name.
121    pub per_command_overrides: HashMap<String, PerCommandConfig>,
122}
123
124/// Per-command overrides. All fields are optional — only specified fields
125/// override the global config for that command.
126#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
127#[serde(deny_unknown_fields)]
128pub struct PerCommandConfig {
129    /// Override the command casing rule for this command only.
130    pub command_case: Option<CaseStyle>,
131    /// Override the keyword casing rule for this command only.
132    pub keyword_case: Option<CaseStyle>,
133    /// Override the line width for this command only.
134    pub line_width: Option<usize>,
135    /// Override the indentation width for this command only.
136    pub tab_size: Option<usize>,
137    /// Override dangling paren placement for this command only.
138    pub dangle_parens: Option<bool>,
139    /// Override dangling paren alignment for this command only.
140    pub dangle_align: Option<DangleAlign>,
141    /// Override the hanging-wrap positional argument threshold for this
142    /// command only.
143    #[serde(rename = "max_hanging_wrap_positional_args")]
144    pub max_pargs_hwrap: Option<usize>,
145    /// Override the hanging-wrap subgroup threshold for this command only.
146    #[serde(rename = "max_hanging_wrap_groups")]
147    pub max_subgroups_hwrap: Option<usize>,
148}
149
150impl Default for Config {
151    fn default() -> Self {
152        Self {
153            line_width: 80,
154            tab_size: 2,
155            use_tabchars: false,
156            max_empty_lines: 1,
157            max_lines_hwrap: 2,
158            max_pargs_hwrap: 6,
159            max_subgroups_hwrap: 2,
160            dangle_parens: false,
161            dangle_align: DangleAlign::Prefix,
162            min_prefix_chars: 4,
163            max_prefix_chars: 10,
164            separate_ctrl_name_with_space: false,
165            separate_fn_name_with_space: false,
166            command_case: CaseStyle::Lower,
167            keyword_case: CaseStyle::Upper,
168            enable_markup: true,
169            reflow_comments: false,
170            first_comment_is_literal: true,
171            literal_comment_pattern: String::new(),
172            bullet_char: "*".to_string(),
173            enum_char: ".".to_string(),
174            fence_pattern: r"^\s*[`~]{3}[^`\n]*$".to_string(),
175            ruler_pattern: r"^[^\w\s]{3}.*[^\w\s]{3}$".to_string(),
176            hashruler_min_length: 10,
177            canonicalize_hashrulers: true,
178            per_command_overrides: HashMap::new(),
179        }
180    }
181}
182
183/// CMake control-flow commands that get `separate_ctrl_name_with_space`.
184const CONTROL_FLOW_COMMANDS: &[&str] = &[
185    "if",
186    "elseif",
187    "else",
188    "endif",
189    "foreach",
190    "endforeach",
191    "while",
192    "endwhile",
193    "break",
194    "continue",
195    "return",
196    "block",
197    "endblock",
198];
199
200/// CMake function/macro definition commands that get
201/// `separate_fn_name_with_space`.
202const FN_DEFINITION_COMMANDS: &[&str] = &["function", "endfunction", "macro", "endmacro"];
203
204impl Config {
205    /// Returns a `Config` with any per-command overrides applied for the
206    /// given command name, plus the appropriate space-before-paren setting.
207    pub fn for_command(&self, command_name: &str) -> CommandConfig<'_> {
208        let lower = command_name.to_ascii_lowercase();
209        let per_cmd = self.per_command_overrides.get(&lower);
210
211        let space_before_paren = if CONTROL_FLOW_COMMANDS.contains(&lower.as_str()) {
212            self.separate_ctrl_name_with_space
213        } else if FN_DEFINITION_COMMANDS.contains(&lower.as_str()) {
214            self.separate_fn_name_with_space
215        } else {
216            false
217        };
218
219        CommandConfig {
220            global: self,
221            per_cmd,
222            space_before_paren,
223        }
224    }
225
226    /// Apply the command_case rule to a command name.
227    pub fn apply_command_case(&self, name: &str) -> String {
228        apply_case(self.command_case, name)
229    }
230
231    /// Apply the keyword_case rule to a keyword token.
232    pub fn apply_keyword_case(&self, keyword: &str) -> String {
233        apply_case(self.keyword_case, keyword)
234    }
235
236    /// The indentation string (spaces or tab).
237    pub fn indent_str(&self) -> String {
238        if self.use_tabchars {
239            "\t".to_string()
240        } else {
241            " ".repeat(self.tab_size)
242        }
243    }
244}
245
246/// A resolved config for formatting a specific command, with per-command
247/// overrides already applied.
248#[derive(Debug)]
249pub struct CommandConfig<'a> {
250    /// The global configuration before per-command overrides are applied.
251    pub global: &'a Config,
252    per_cmd: Option<&'a PerCommandConfig>,
253    /// Whether this command should render a space before `(`.
254    pub space_before_paren: bool,
255}
256
257impl CommandConfig<'_> {
258    /// Effective line width for the current command.
259    pub fn line_width(&self) -> usize {
260        self.per_cmd
261            .and_then(|p| p.line_width)
262            .unwrap_or(self.global.line_width)
263    }
264
265    /// Effective indentation width for the current command.
266    pub fn tab_size(&self) -> usize {
267        self.per_cmd
268            .and_then(|p| p.tab_size)
269            .unwrap_or(self.global.tab_size)
270    }
271
272    /// Effective dangling-paren setting for the current command.
273    pub fn dangle_parens(&self) -> bool {
274        self.per_cmd
275            .and_then(|p| p.dangle_parens)
276            .unwrap_or(self.global.dangle_parens)
277    }
278
279    /// Effective dangling-paren alignment for the current command.
280    pub fn dangle_align(&self) -> DangleAlign {
281        self.per_cmd
282            .and_then(|p| p.dangle_align)
283            .unwrap_or(self.global.dangle_align)
284    }
285
286    /// Effective command casing rule for the current command.
287    pub fn command_case(&self) -> CaseStyle {
288        self.per_cmd
289            .and_then(|p| p.command_case)
290            .unwrap_or(self.global.command_case)
291    }
292
293    /// Effective keyword casing rule for the current command.
294    pub fn keyword_case(&self) -> CaseStyle {
295        self.per_cmd
296            .and_then(|p| p.keyword_case)
297            .unwrap_or(self.global.keyword_case)
298    }
299
300    /// Effective hanging-wrap positional argument threshold for the current
301    /// command.
302    pub fn max_pargs_hwrap(&self) -> usize {
303        self.per_cmd
304            .and_then(|p| p.max_pargs_hwrap)
305            .unwrap_or(self.global.max_pargs_hwrap)
306    }
307
308    /// Effective hanging-wrap subgroup threshold for the current command.
309    pub fn max_subgroups_hwrap(&self) -> usize {
310        self.per_cmd
311            .and_then(|p| p.max_subgroups_hwrap)
312            .unwrap_or(self.global.max_subgroups_hwrap)
313    }
314
315    /// Effective indentation unit for the current command.
316    pub fn indent_str(&self) -> String {
317        if self.global.use_tabchars {
318            "\t".to_string()
319        } else {
320            " ".repeat(self.tab_size())
321        }
322    }
323}
324
325fn apply_case(style: CaseStyle, s: &str) -> String {
326    match style {
327        CaseStyle::Lower => s.to_ascii_lowercase(),
328        CaseStyle::Upper => s.to_ascii_uppercase(),
329        CaseStyle::Unchanged => s.to_string(),
330    }
331}