use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
const SELF: &str = "wasm_compat_guard.rs";
struct ForbiddenApi {
needle: &'static str,
line_must_also_contain: &'static [&'static str],
why: &'static str,
allowed: &'static [(&'static str, &'static str)],
}
const FORBIDDEN: &[ForbiddenApi] = &[
ForbiddenApi {
needle: "std::time",
line_must_also_contain: &["Instant", "SystemTime"],
why: "`std::time::Instant::now()` and `SystemTime::now()` panic on \
wasm32-unknown-unknown. Use the `web-time` crate (already a dependency), \
which re-exports the std types on native targets and browser-backed ones \
on wasm — see `input/state.rs` for the pattern.",
allowed: &[],
},
ForbiddenApi {
needle: "std::fs",
line_must_also_contain: &[],
why: "there is no filesystem on wasm32-unknown-unknown; `std::fs` compiles and then \
errors at runtime. Bundle files with the asset system, or document the API as \
native-only and add it to this allowlist.",
allowed: &[
(
"fs.rs",
"the file-editing module exists to read/write the local disk; native-only by design and documented as such",
),
(
"keymap/mod.rs",
"loads keymap JSON from user-supplied paths; native-only by design and documented as such",
),
(
"editor/syntax_highlighter.rs",
"`load_theme_from_file` reads a .tmTheme from disk; native-only by design and documented as such",
),
(
"a11y.rs",
"test-only: golden-file assertions inside `#[cfg(test)] mod tests`",
),
(
"element_id.rs",
"test-only: source scan inside `#[cfg(test)] mod tests`",
),
("build_profile_guard.rs", "test-only guard module"),
("doctest_fence_guard.rs", "test-only guard module"),
("release_input_validation.rs", "test-only guard module"),
("release_version_guard.rs", "test-only guard module"),
("undying_thread_guard.rs", "test-only guard module"),
],
},
ForbiddenApi {
needle: "std::thread::spawn",
line_must_also_contain: &[],
why: "spawning OS threads traps on wasm32-unknown-unknown (gpui's web platform runs \
background work on web workers instead). Use gpui's BackgroundExecutor.",
allowed: &[(
"utils/element_manager.rs",
"test-only: a thread-safety test inside `#[cfg(test)] mod tests`",
)],
},
];
fn repo_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
}
fn code_only(source: &str) -> String {
let mut out = String::with_capacity(source.len());
let mut chars = source.chars().peekable();
let mut in_block = false;
while let Some(c) = chars.next() {
if in_block {
if c == '*' && chars.peek() == Some(&'/') {
chars.next();
in_block = false;
}
continue;
}
if c == '/' {
match chars.peek() {
Some('/') => {
for c in chars.by_ref() {
if c == '\n' {
out.push('\n');
break;
}
}
continue;
}
Some('*') => {
chars.next();
in_block = true;
continue;
}
_ => {}
}
}
out.push(c);
}
out
}
fn rust_files(dir: &Path) -> Vec<PathBuf> {
let mut files = Vec::new();
let mut stack = vec![dir.to_path_buf()];
while let Some(dir) = stack.pop() {
for entry in std::fs::read_dir(&dir).expect("source directory is readable") {
let path = entry.expect("directory entry is readable").path();
if path.is_dir() {
stack.push(path);
} else if path.extension().is_some_and(|ext| ext == "rs") {
files.push(path);
}
}
}
files.sort();
files
}
fn uses_api(code: &str, api: &ForbiddenApi) -> bool {
code.lines().any(|line| {
line.contains(api.needle)
&& (api.line_must_also_contain.is_empty()
|| api
.line_must_also_contain
.iter()
.any(|extra| line.contains(extra)))
})
}
#[test]
fn wasm_hostile_apis_appear_only_where_allowed() {
let src = repo_root().join("src");
let files = rust_files(&src);
assert!(
files.len() > 20,
"walked src/ and found {} Rust files — the walker is broken",
files.len(),
);
let mut scanned = 0usize;
let mut matched: Vec<(usize, String)> = Vec::new();
for file in &files {
if file.file_name().is_some_and(|name| name == SELF) {
continue;
}
scanned += 1;
let relative = file
.strip_prefix(&src)
.expect("file is under src/")
.to_str()
.expect("source paths are UTF-8")
.to_string();
let code = code_only(&std::fs::read_to_string(file).expect("source file is readable"));
for (index, api) in FORBIDDEN.iter().enumerate() {
if !uses_api(&code, api) {
continue;
}
let allowed = api.allowed.iter().any(|(path, _)| *path == relative);
assert!(
allowed,
"{relative} uses `{}`{}, which is not on the allowlist: {} \
If this use is genuinely native-only or test-only, add an entry to \
`FORBIDDEN` in {SELF} saying which; otherwise use the wasm-safe \
alternative the message names.",
api.needle,
if api.line_must_also_contain.is_empty() {
String::new()
} else {
format!(" (with {:?})", api.line_must_also_contain)
},
api.why,
);
matched.push((index, relative.clone()));
}
}
assert_eq!(
scanned,
files.len() - 1,
"exactly one file — `{SELF}`, which names the forbidden strings in its own \
literals — is exempt from this scan. {} were skipped.",
files.len() - scanned,
);
let matched: BTreeSet<(usize, String)> = matched.into_iter().collect();
for (index, api) in FORBIDDEN.iter().enumerate() {
for (path, justification) in api.allowed {
assert!(
matched.contains(&(index, (*path).to_string())),
"src/{path} no longer uses `{}` — delete its allowlist entry from {SELF} \
(it was allowed because: {justification}).",
api.needle,
);
}
}
}