Skip to main content

miniextendr_lint/
lib.rs

1//! miniextendr-lint: internal build-time lint helpers for the workspace.
2//!
3//! This crate scans Rust sources for miniextendr macro usage and emits
4//! cargo warnings with actionable diagnostics. It is intended for local
5//! development and CI, not as a public API.
6//!
7//! ## Usage in build.rs
8//!
9//! ```ignore
10//! fn main() {
11//!     miniextendr_lint::build_script();
12//! }
13//! ```
14//!
15//! ## Configuration
16//! - Controlled by the `MINIEXTENDR_LINT` env var (enabled by default).
17//! - Set it to `0`, `false`, `no`, or `off` to disable.
18//!
19//! ## Lint Codes
20//!
21//! Each diagnostic carries a stable `MXL###` code. See [`LintCode`] for the full catalog.
22
23pub mod crate_index;
24pub mod crate_root;
25pub mod description;
26pub mod diagnostic;
27mod doc_links;
28pub mod helpers;
29pub mod lint_code;
30mod misplaced_dots;
31pub mod rules;
32
33use std::env;
34use std::path::{Path, PathBuf};
35
36pub use crate_index::{CrateIndex, IndexError, LintItem, LintKind};
37pub use crate_root::{CrateRoot, RootOrigin};
38pub use diagnostic::{Diagnostic, Severity};
39pub use lint_code::LintCode;
40
41/// A `cargo::warning` directive for `message`, on one line.
42fn warning_directive(message: &str) -> String {
43    let message = message.replace(['\n', '\r'], " ");
44    ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("cargo::warning={0}",
                message.trim()))
    })format!("cargo::warning={}", message.trim())
45}
46
47/// A `cargo::rerun-if-changed` directive for `path`.
48fn rerun_directive(path: &Path) -> String {
49    ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("cargo::rerun-if-changed={0}",
                path.display()))
    })format!("cargo::rerun-if-changed={}", path.display())
50}
51
52/// Entry point for build.rs. Runs the lint and prints cargo directives.
53///
54/// Controlled by `MINIEXTENDR_LINT` env var (enabled by default).
55/// Set to `0`, `false`, `no`, or `off` to disable.
56pub fn build_script() {
57    {
    ::std::io::_print(format_args!("cargo::rerun-if-env-changed=MINIEXTENDR_LINT\n"));
};println!("cargo::rerun-if-env-changed=MINIEXTENDR_LINT");
58
59    let enabled = match lint_enabled("MINIEXTENDR_LINT") {
60        Ok(enabled) => enabled,
61        Err(message) => {
62            { ::std::io::_print(format_args!("{0}\n", warning_directive(&message))); };println!("{}", warning_directive(&message));
63            return;
64        }
65    };
66
67    if !enabled {
68        return;
69    }
70
71    let manifest_dir = match env::var("CARGO_MANIFEST_DIR") {
72        Ok(dir) => PathBuf::from(dir),
73        Err(err) => {
74            {
    ::std::io::_print(format_args!("{0}\n",
            warning_directive(&::alloc::__export::must_use({
                            ::alloc::fmt::format(format_args!("CARGO_MANIFEST_DIR: {0}",
                                    err))
                        }))));
};println!(
75                "{}",
76                warning_directive(&format!("CARGO_MANIFEST_DIR: {err}"))
77            );
78            return;
79        }
80    };
81
82    for directive in build_directives(&manifest_dir) {
83        { ::std::io::_print(format_args!("{0}\n", directive)); };println!("{directive}");
84    }
85
86    // Note: generate_link_registrations() was removed. Wrapper generation now
87    // loads the freshly linked package library. The staticlib build uses
88    // codegen-units = 1 so the linker pulls all linkme entries via stub.c anchor.
89}
90
91/// The cargo directives [`build_script`] prints for the crate at `manifest_dir`
92/// once the lint is enabled: what to watch, then the warnings.
93///
94/// Once a build script prints a `rerun-if-*` directive, cargo reruns it only when
95/// one of them fires and replays its cached warnings in between, so every source
96/// file the lint found is watched on the error path too: a "failed to parse"
97/// warning must not outlive the fix (#1738). A `src/` directory holding the crate
98/// root is also watched as a whole (cargo scans it recursively), which covers a
99/// module file created after the `mod` line naming it. A crate root anywhere
100/// else, such as the `lib.rs` next to `Cargo.toml` of every scaffolded R package,
101/// is watched file by file only: a recursive watch of the manifest directory
102/// would take in `target/`, `vendor/` and `.cargo/` and rerun the script after
103/// every build.
104///
105/// `Cargo.toml` holds `[lib] path` (#1745) but is watched only while no crate
106/// root file can be found or read. Then no source file is watched, and without
107/// the manifest nothing would ever rerun the lint to clear its warning. A
108/// healthy crate leaves it unwatched: every scaffolded `configure` touches
109/// `Cargo.toml`, and watching it would rebuild the crate on every install (a
110/// crate cargo builds but the lint cannot read, such as an inline
111/// `lib = { path = ... }`, pays that cost until the spelling is rewritten).
112/// Moving the root file away is still seen, since a watched file that
113/// disappears reruns the script (#1752).
114pub fn build_directives(manifest_dir: &Path) -> Vec<String> {
115    let mut directives = Vec::new();
116
117    let root = CrateRoot::resolve(manifest_dir);
118    let manifest = manifest_dir.join("Cargo.toml");
119    let root_found = #[allow(non_exhaustive_omitted_patterns)] match &root {
    Ok(root) if root.file.is_file() => true,
    _ => false,
}matches!(&root, Ok(root) if root.file.is_file());
120    if !root_found && manifest.is_file() {
121        directives.push(rerun_directive(&manifest));
122    }
123
124    let src_dir = manifest_dir.join("src");
125    if let Ok(root) = &root
126        && root.file.starts_with(&src_dir)
127    {
128        directives.push(rerun_directive(&src_dir));
129    }
130
131    // The doc-link rule (MXL204) reads `DESCRIPTION`, so adding a dependency
132    // there must rerun it.
133    if let Some(description) = description::locate(manifest_dir) {
134        directives.push(rerun_directive(&description));
135    }
136
137    match run(manifest_dir) {
138        Ok(report) => {
139            directives.extend(report.files.iter().map(|path| rerun_directive(path)));
140            if !report.diagnostics.is_empty() {
141                directives.push(warning_directive("miniextendr-lint found issues"));
142                directives.extend(
143                    report
144                        .diagnostics
145                        .iter()
146                        .map(|diag| warning_directive(&diag.to_string())),
147                );
148            }
149        }
150        Err(err) => {
151            directives.extend(err.files.iter().map(|path| rerun_directive(path)));
152            directives.push(warning_directive(&err.message));
153        }
154    }
155
156    directives
157}
158
159#[derive(#[automatically_derived]
impl ::core::fmt::Debug for LintReport {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_struct_field3_finish(f, "LintReport",
            "files", &self.files, "diagnostics", &self.diagnostics, "errors",
            &&self.errors)
    }
}Debug, #[automatically_derived]
impl ::core::default::Default for LintReport {
    #[inline]
    fn default() -> Self {
        LintReport {
            files: ::core::default::Default::default(),
            diagnostics: ::core::default::Default::default(),
            errors: ::core::default::Default::default(),
        }
    }
}Default)]
160/// Result of running the lint over a crate source tree.
161pub struct LintReport {
162    /// Rust source files that were scanned.
163    pub files: Vec<PathBuf>,
164    /// Structured diagnostics from all rules.
165    pub diagnostics: Vec<Diagnostic>,
166    /// Legacy string errors (derived from diagnostics, for backward compatibility).
167    pub errors: Vec<String>,
168}
169
170/// Returns whether the lint should run based on the given env var.
171///
172/// Defaults to `true` when the var is unset. Set to 0/false/no/off to disable.
173pub fn lint_enabled(env_var: &str) -> Result<bool, String> {
174    match env::var(env_var) {
175        Ok(value) => {
176            let normalized = value.trim().to_ascii_lowercase();
177            match normalized.as_str() {
178                "0" | "false" | "no" | "off" | "" => Ok(false),
179                "1" | "true" | "yes" | "on" => Ok(true),
180                _ => Err(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0} has invalid value \'{1}\'; use 1/0, true/false, yes/no, on/off",
                env_var, value))
    })format!(
181                    "{env_var} has invalid value '{value}'; use 1/0, true/false, yes/no, on/off"
182                )),
183            }
184        }
185        Err(env::VarError::NotPresent) => Ok(true),
186        Err(err) => Err(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0}: {1}", env_var, err))
    })format!("{env_var}: {err}")),
187    }
188}
189
190/// Run the lint against the crate whose `Cargo.toml` sits in `manifest_dir`.
191///
192/// The module walk starts from `[lib] path` in `Cargo.toml`; without that key,
193/// from `src/lib.rs` when `src/` exists, else from `lib.rs` ([`CrateRoot`]).
194pub fn run(manifest_dir: impl AsRef<Path>) -> Result<LintReport, IndexError> {
195    let index = CrateIndex::build(manifest_dir.as_ref())?;
196    let diagnostics = rules::run_all_rules(&index);
197
198    let errors = diagnostics
199        .iter()
200        .filter(|d| d.severity == Severity::Error)
201        .map(|d| d.to_legacy_string())
202        .collect();
203
204    Ok(LintReport {
205        files: index.files,
206        diagnostics,
207        errors,
208    })
209}