Skip to main content

git_policy_cli/
markdown_linter_lookup.rs

1//! What:
2//!  Find the `monochromatic-lint` executable the `markdown/autofix` policy runs,
3//!  on
4//!       the PATH of the invocation's environment.
5//! Why:
6//!  Repository configuration cannot choose the program (`doc/planning/cli-git-rust-implementation.md`,
7//!      lines 181 to 189:
8//!  first-party linter selection is installation wiring).
9//!  The linter
10//!      is installed through mise as `cargo:monochromatic-lint`,
11//!  and mise exposes installed
12//!      tools on PATH,
13//!  through `mise activate` or its shims directory;
14//!  real Git is found on
15//!      PATH the same way.
16//!  Only absolute PATH entries are searched:
17//!  a relative or empty entry
18//!      names a directory relative to where the command runs,
19//!  usually inside the very
20//!      repository whose content the linter checks,
21//!  and that repository must not be able to
22//!      supply the program.
23//!
24//! In TS you'd write (pseudocode):
25//! ```ts
26//! // const linter = findLinter(env.PATH); // '/home/u/.local/share/mise/shims/monochromatic-lint'
27//! ```
28
29/// Import the executable-bit test real-Git lookup uses.
30use super::real_git_candidate::is_executable;
31/// `OsStr` is borrowed operating-system text:
32///  PATH need not be UTF-8.
33use std::ffi::OsStr;
34/// `PathBuf` is an owned filesystem path.
35use std::path::PathBuf;
36
37/// The linter's executable name without the platform's suffix.
38pub const LINTER_NAME: &str = "monochromatic-lint";
39
40/// What:
41///  The linter's file name on this platform:
42///  `monochromatic-lint`,
43///  or with `.exe` on
44///       Windows.
45///  `std::env::consts::EXE_SUFFIX` is the platform's executable suffix.
46/// Why:
47///   Cargo installs the executable under exactly this name.
48///
49/// In TS you'd write (pseudocode):
50/// ```ts
51/// const linterFileName = () => process.platform === 'win32' ? 'monochromatic-lint.exe' : 'monochromatic-lint';
52/// ```
53pub fn linter_file_name() -> String {
54    return format!("{LINTER_NAME}{}", std::env::consts::EXE_SUFFIX);
55}
56
57/// What:
58///  The first absolute PATH entry holding an executable regular file of the linter's
59///       name,
60///  following symbolic links.
61///  `Option<&OsStr>` is the PATH value or nothing.
62/// Why:
63///   This is the shell's lookup order,
64///  without the entries that would let the
65///       checked repository choose the program.
66///  `None` means the linter is not installed
67///       where this invocation can see it,
68///  which the policy reports as a failure.
69///
70/// In TS you'd write (pseudocode):
71/// ```ts
72/// function findLinter(path?: string): string | undefined;
73/// ```
74pub fn find_linter(path_variable: Option<&OsStr>) -> Option<PathBuf> {
75    // A trailing `?` on an absent PATH returns `None` at once: there is nothing to search.
76    let path: &OsStr = path_variable?;
77    let name: String = linter_file_name();
78    // `std::env::split_paths` cuts PATH by the platform's separator.
79    for directory in std::env::split_paths(path) {
80        if !directory.is_absolute() {
81            continue;
82        }
83        let candidate: PathBuf = directory.join(name.as_str());
84        // `std::fs::metadata` follows symbolic links, as running the file would.
85        if let Ok(metadata) = std::fs::metadata(&candidate)
86            && metadata.is_file()
87            && is_executable(&metadata)
88        {
89            return Some(candidate);
90        }
91    }
92    return None;
93}
94
95/// Lookup-order controls stay out of the release executable.
96#[cfg(test)]
97#[path = "markdown_linter_lookup_tests.rs"]
98mod tests;