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