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;