wowlab_tidy/languages/toml/rules/cargo/
mimalloc_apps.rs1#[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
12const 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}