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 diagnostic;
25pub mod helpers;
26pub mod lint_code;
27pub mod rules;
28
29use std::env;
30use std::path::{Path, PathBuf};
31
32pub use crate_index::{CrateIndex, LintItem, LintKind};
33pub use diagnostic::{Diagnostic, Severity};
34pub use lint_code::LintCode;
35
36/// Emits a single cargo warning line after normalizing whitespace.
37fn cargo_warning(message: &str) {
38    let message = message.replace(['\n', '\r'], " ");
39    println!("cargo::warning={}", message.trim());
40}
41
42/// Entry point for build.rs. Runs the lint and prints cargo directives.
43///
44/// Controlled by `MINIEXTENDR_LINT` env var (enabled by default).
45/// Set to `0`, `false`, `no`, or `off` to disable.
46pub fn build_script() {
47    println!("cargo::rerun-if-env-changed=MINIEXTENDR_LINT");
48
49    let enabled = match lint_enabled("MINIEXTENDR_LINT") {
50        Ok(enabled) => enabled,
51        Err(message) => {
52            cargo_warning(&message);
53            return;
54        }
55    };
56
57    if !enabled {
58        return;
59    }
60
61    let manifest_dir = match env::var("CARGO_MANIFEST_DIR") {
62        Ok(dir) => PathBuf::from(dir),
63        Err(err) => {
64            cargo_warning(&format!("CARGO_MANIFEST_DIR: {err}"));
65            return;
66        }
67    };
68
69    let report = match run(&manifest_dir) {
70        Ok(report) => report,
71        Err(message) => {
72            cargo_warning(&message);
73            return;
74        }
75    };
76
77    for path in &report.files {
78        println!("cargo::rerun-if-changed={}", path.display());
79    }
80
81    if !report.diagnostics.is_empty() {
82        cargo_warning("miniextendr-lint found issues");
83        for diag in &report.diagnostics {
84            cargo_warning(&diag.to_string());
85        }
86    }
87
88    // Note: generate_link_registrations() was removed — the document binary
89    // was replaced by cdylib-based wrapper generation. The staticlib build uses
90    // codegen-units = 1 so the linker pulls all linkme entries via stub.c anchor.
91}
92
93#[derive(Debug, Default)]
94/// Result of running the lint over a crate source tree.
95pub struct LintReport {
96    /// Rust source files that were scanned.
97    pub files: Vec<PathBuf>,
98    /// Structured diagnostics from all rules.
99    pub diagnostics: Vec<Diagnostic>,
100    /// Legacy string errors (derived from diagnostics, for backward compatibility).
101    pub errors: Vec<String>,
102}
103
104/// Returns whether the lint should run based on the given env var.
105///
106/// Defaults to `true` when the var is unset. Set to 0/false/no/off to disable.
107pub fn lint_enabled(env_var: &str) -> Result<bool, String> {
108    match env::var(env_var) {
109        Ok(value) => {
110            let normalized = value.trim().to_ascii_lowercase();
111            match normalized.as_str() {
112                "0" | "false" | "no" | "off" | "" => Ok(false),
113                "1" | "true" | "yes" | "on" => Ok(true),
114                _ => Err(format!(
115                    "{env_var} has invalid value '{value}'; use 1/0, true/false, yes/no, on/off"
116                )),
117            }
118        }
119        Err(env::VarError::NotPresent) => Ok(true),
120        Err(err) => Err(format!("{env_var}: {err}")),
121    }
122}
123
124/// Run the lint against the crate rooted at `root`.
125///
126/// If `root/src` exists, that directory is scanned. Otherwise `root` is scanned.
127pub fn run(root: impl AsRef<Path>) -> Result<LintReport, String> {
128    let root = root.as_ref();
129
130    let index = CrateIndex::build(root)?;
131    let diagnostics = rules::run_all_rules(&index);
132
133    let errors = diagnostics
134        .iter()
135        .filter(|d| d.severity == Severity::Error)
136        .map(|d| d.to_legacy_string())
137        .collect();
138
139    Ok(LintReport {
140        files: index.files,
141        diagnostics,
142        errors,
143    })
144}