Skip to main content

wowlab_tidy/languages/toml/rules/cargo/
mimalloc_apps.rs

1#[cfg(test)]
2use googletest::prelude::*;
3use wowlab_fs::{
4    directory::{self, EntryKind},
5    file,
6    path::Path,
7};
8
9use super::is_workspace_member_manifest;
10use crate::{Example, TomlCtx, Violation, violation};
11
12// Split so tidy's own sources never satisfy the allocator scan.
13const GLOBAL_ALLOCATOR_ATTR: &str = concat!("#[global_", "allocator]");
14
15#[rustfmt::skip]
16const EXAMPLES: &[Example] = &[
17    Example { label: "library crate", code: "[package]\nname = \"foo\"\n\n[dependencies]\nserde = \"1\"\n", pass: true },
18    Example { label: "binary with mimalloc", code: "[package]\nname = \"foo\"\n\n[[bin]]\nname = \"foo\"\n\n[dependencies]\nmimalloc = \"0.1\"\n", pass: true },
19    Example { label: "wasm binary exempt", code: "[package]\nname = \"foo\"\n\n[[bin]]\nname = \"foo\"\n\n[dependencies]\nwasm-bindgen = \"0.2\"\n", pass: true },
20    Example { label: "binary without mimalloc", code: "[package]\nname = \"foo\"\n\n[[bin]]\nname = \"foo\"\n\n[dependencies]\nserde = \"1\"\n", pass: false },
21    Example { label: "bin crate-type without mimalloc", code: "[package]\nname = \"foo\"\n\n[lib]\ncrate-type = [\"bin\"]\n", pass: false },
22];
23
24crate::toml_rule!(
25    toml_cargo_mimalloc_apps,
26    "Require binary crates to depend on mimalloc and install it as the global allocator.",
27    "mimalloc as the global allocator is significant performance at no cost for applications (M-MIMALLOC-APPS).",
28    Low,
29);
30
31fn check_toml_cargo_mimalloc_apps(ctx: &TomlCtx<'_>) -> Vec<Violation> {
32    if !is_workspace_member_manifest(ctx.file.rel) || !ctx.parse.errors.is_empty() {
33        return Vec::new();
34    }
35
36    let Ok(document) = toml::from_str::<toml::Table>(ctx.file.contents) else {
37        return Vec::new();
38    };
39
40    if !defines_binary(&document, ctx.file.path) || depends_on(&document, "wasm-bindgen") {
41        return Vec::new();
42    }
43
44    let mut violations = Vec::new();
45    let has_mimalloc = document
46        .get("dependencies")
47        .and_then(toml::Value::as_table)
48        .is_some_and(|deps| {
49            deps.iter()
50                .any(|(key, value)| resolved_name(key, value) == "mimalloc")
51        });
52
53    if !has_mimalloc {
54        violations.push(violation(
55            ctx.file.rel,
56            1,
57            "binary crate must add mimalloc to [dependencies] (M-MIMALLOC-APPS)",
58        ));
59    }
60
61    if let Some(false) = src_has_global_allocator(ctx.file.path) {
62        violations.push(violation(
63            ctx.file.rel,
64            1,
65            format!(
66                "binary crate must install mimalloc via {GLOBAL_ALLOCATOR_ATTR} in its sources (M-MIMALLOC-APPS)"
67            ),
68        ));
69    }
70
71    violations
72}
73
74fn defines_binary(document: &toml::Table, manifest_path: &Path) -> bool {
75    if document
76        .get("bin")
77        .and_then(toml::Value::as_array)
78        .is_some_and(|targets| !targets.is_empty())
79    {
80        return true;
81    }
82
83    let bin_crate_type = document
84        .get("lib")
85        .and_then(toml::Value::as_table)
86        .and_then(|lib| lib.get("crate-type"))
87        .and_then(toml::Value::as_array)
88        .is_some_and(|kinds| {
89            kinds
90                .iter()
91                .filter_map(toml::Value::as_str)
92                .any(|kind| kind == "bin")
93        });
94
95    if bin_crate_type {
96        return true;
97    }
98
99    manifest_path.parent().is_some_and(|dir| {
100        directory::inspect(&dir.join("src/main.rs"))
101            .ok()
102            .flatten()
103            .is_some_and(|entry| entry.kind() == EntryKind::File)
104    })
105}
106
107fn src_has_global_allocator(manifest_path: &Path) -> Option<bool> {
108    let src = manifest_path.parent()?.join("src");
109
110    if !directory::inspect(&src)
111        .ok()
112        .flatten()
113        .is_some_and(|entry| entry.kind() == EntryKind::Directory)
114    {
115        return None;
116    }
117
118    let paths = crate::infra::walk::rs_paths(&src, &[]).ok()?;
119    let installs = paths.iter().any(|path| {
120        file::read_text(path).is_ok_and(|source| source.contains(GLOBAL_ALLOCATOR_ATTR))
121    });
122
123    Some(installs)
124}
125
126fn depends_on(document: &toml::Table, name: &str) -> bool {
127    dependency_tables(document)
128        .into_iter()
129        .flat_map(|table| table.iter())
130        .any(|(key, value)| resolved_name(key, value) == name)
131}
132
133fn dependency_tables(document: &toml::Table) -> Vec<&toml::Table> {
134    const DEP_TABLES: &[&str] = &["dependencies", "dev-dependencies", "build-dependencies"];
135    let mut tables = Vec::new();
136
137    for name in DEP_TABLES {
138        if let Some(table) = document.get(*name).and_then(toml::Value::as_table) {
139            tables.push(table);
140        }
141    }
142
143    if let Some(targets) = document.get("target").and_then(toml::Value::as_table) {
144        for target in targets.values().filter_map(toml::Value::as_table) {
145            for name in DEP_TABLES {
146                if let Some(table) = target.get(*name).and_then(toml::Value::as_table) {
147                    tables.push(table);
148                }
149            }
150        }
151    }
152
153    tables
154}
155
156fn resolved_name<'a>(key: &'a str, value: &'a toml::Value) -> &'a str {
157    value
158        .as_table()
159        .and_then(|table| table.get("package"))
160        .and_then(toml::Value::as_str)
161        .unwrap_or(key)
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167
168    fn run(source: &str) -> Vec<Violation> {
169        crate::test_support::check_source_toml_at(
170            "crates/foo/Cargo.toml",
171            source,
172            check_toml_cargo_mimalloc_apps,
173        )
174    }
175
176    crate::example_tests!(EXAMPLES, check_toml_cargo_mimalloc_apps);
177
178    #[gtest]
179    fn renamed_mimalloc_dependency_counts() -> Result<()> {
180        let violations = run(
181            "[package]\nname = \"foo\"\n\n[[bin]]\nname = \"foo\"\n\n[dependencies]\nalloc = { package = \"mimalloc\", version = \"0.1\" }\n",
182        );
183
184        verify_true!(violations.is_empty())?;
185
186        Ok(())
187    }
188}