Skip to main content

cmakefmt/
files.rs

1// SPDX-FileCopyrightText: Copyright 2026 Puneet Matharu
2//
3// SPDX-License-Identifier: MIT OR Apache-2.0
4
5use std::ffi::OsStr;
6use std::path::{Path, PathBuf};
7
8use ignore::WalkBuilder;
9use regex::Regex;
10
11/// User-facing custom ignore filename honored during recursive discovery.
12pub const CUSTOM_IGNORE_FILE_NAME: &str = ".cmakefmtignore";
13
14/// Options controlling recursive CMake file discovery.
15#[derive(Debug, Clone, Default)]
16pub struct DiscoveryOptions<'a> {
17    /// Optional regex filter applied after filename/ignore filtering.
18    pub file_filter: Option<&'a Regex>,
19    /// Honor Git ignore files while walking directories.
20    pub honor_gitignore: bool,
21    /// Additional ignore files loaded explicitly by the user.
22    pub explicit_ignore_paths: &'a [PathBuf],
23}
24
25/// Recursively discover CMake files below `root`, optionally filtering the
26/// discovered paths with `file_filter`.
27///
28/// Returned paths are sorted to keep CLI output and batch formatting stable.
29pub fn discover_cmake_files(root: &Path, file_filter: Option<&Regex>) -> Vec<PathBuf> {
30    discover_cmake_files_with_options(
31        root,
32        DiscoveryOptions {
33            file_filter,
34            honor_gitignore: false,
35            explicit_ignore_paths: &[],
36        },
37    )
38}
39
40/// Recursively discover CMake files below `root` using the provided workflow
41/// options, including ignore-file handling.
42pub fn discover_cmake_files_with_options(
43    root: &Path,
44    options: DiscoveryOptions<'_>,
45) -> Vec<PathBuf> {
46    let mut builder = WalkBuilder::new(root);
47    builder.hidden(false);
48    builder.git_ignore(options.honor_gitignore);
49    builder.git_global(options.honor_gitignore);
50    builder.git_exclude(options.honor_gitignore);
51    builder.require_git(false);
52    builder.add_custom_ignore_filename(CUSTOM_IGNORE_FILE_NAME);
53
54    for ignore_path in options.explicit_ignore_paths {
55        builder.add_ignore(ignore_path);
56    }
57
58    let mut files: Vec<_> = builder
59        .build()
60        .filter_map(Result::ok)
61        .filter(|entry| entry.file_type().is_some_and(|kind| kind.is_file()))
62        .map(|entry| entry.into_path())
63        .filter(|path| is_cmake_file(path))
64        .filter(|path| matches_filter(path, options.file_filter))
65        .collect();
66    files.sort();
67    files
68}
69
70/// Returns `true` when the path matches one of the built-in CMake filename
71/// patterns understood by `cmakefmt`.
72///
73/// Supported patterns are:
74///
75/// - `CMakeLists.txt`
76/// - `*.cmake`
77/// - `*.cmake.in`
78pub fn is_cmake_file(path: &Path) -> bool {
79    let Some(file_name) = path.file_name().and_then(OsStr::to_str) else {
80        return false;
81    };
82
83    if file_name == "CMakeLists.txt" {
84        return true;
85    }
86
87    file_name.ends_with(".cmake") || file_name.ends_with(".cmake.in")
88}
89
90/// Returns `true` when `path` matches the optional user-supplied discovery
91/// regex.
92///
93/// When no regex is supplied, every discovered CMake file matches.
94pub fn matches_filter(path: &Path, file_filter: Option<&Regex>) -> bool {
95    let Some(file_filter) = file_filter else {
96        return true;
97    };
98
99    file_filter.is_match(&path.to_string_lossy())
100}