#![expect(
clippy::expect_used,
clippy::panic,
reason = "`clippy.toml`'s allow-*-in-tests only reaches `#[test]` functions and \
`#[cfg(test)]` modules. The helpers below are neither, so the grant it \
already makes for unit tests has to be restated for them."
)]
use std::collections::BTreeSet;
const HOST: &str = include_str!("../src/host.rs");
const TYPES: &str = include_str!("../../../packages/lanekeep/index.d.ts");
fn registered() -> BTreeSet<String> {
let body = HOST.split("#[cfg(test)]").next().unwrap_or(HOST);
let mut names = BTreeSet::new();
for (index, _) in body.match_indices("object.set(") {
let rest = &body[index + "object.set(".len()..];
let Some(open) = rest.find('"') else { continue };
if rest[..open].contains(')') {
continue;
}
let after = &rest[open + 1..];
let Some(close) = after.find('"') else {
continue;
};
names.insert(after[..close].to_owned());
}
names
}
fn declared() -> BTreeSet<String> {
let mut names = BTreeSet::new();
for interface in [
"export interface RuleContext {",
"export interface ReduceContext {",
] {
let start = TYPES
.find(interface)
.unwrap_or_else(|| panic!("`{interface}` is missing from the definitions"));
let body = &TYPES[start + interface.len()..];
let end = body.find("\n}").expect("the interface is closed");
for line in body[..end].lines() {
let line = line.trim();
if line.is_empty()
|| line.starts_with("//")
|| line.starts_with('*')
|| line.starts_with("/*")
{
continue;
}
let line = line.strip_prefix("readonly ").unwrap_or(line);
let name: String = line
.chars()
.take_while(|c| c.is_ascii_alphanumeric() || *c == '_')
.collect();
if name.is_empty() {
continue;
}
let after = &line[name.len()..];
if after.starts_with('(') || after.starts_with(':') || after.starts_with("?(") {
names.insert(name);
}
}
}
names
}
const NOT_CONTEXT_MEMBERS: &[&str] = &["file", "loc"];
#[test]
fn the_types_claim_nothing_the_host_does_not_provide() {
let registered = registered();
let declared = declared();
let invented: Vec<&String> = declared
.iter()
.filter(|name| !registered.contains(*name))
.collect();
assert!(
invented.is_empty(),
"packages/lanekeep/index.d.ts declares {invented:?}, which host.rs does not register. \
An author would get autocomplete for a method that throws at run time."
);
}
#[test]
fn the_types_cover_everything_the_host_provides() {
let declared = declared();
let registered = registered();
let missing: Vec<&String> = registered
.iter()
.filter(|name| !declared.contains(*name))
.filter(|name| !NOT_CONTEXT_MEMBERS.contains(&name.as_str()))
.collect();
assert!(
missing.is_empty(),
"host.rs registers {missing:?}, which packages/lanekeep/index.d.ts does not declare. \
The method works but is invisible to anyone writing a rule."
);
}
#[test]
fn every_binding_kind_the_resolvers_can_return_is_typed() {
const KINDS: &str = include_str!("../../lanekeep-lang/src/binding.rs");
let arms = KINDS
.split("pub const fn as_str(self)")
.nth(1)
.expect("BindingKind::as_str is where the strings live");
let arms = &arms[..arms.find("\n }").expect("the function is closed")];
let mut found = 0_usize;
let mut missing = Vec::new();
for line in arms.lines() {
let line = line.trim();
let Some(rest) = line.strip_prefix("Self::") else {
continue;
};
let Some(start) = rest.find('"') else {
continue;
};
let after = &rest[start + 1..];
let Some(end) = after.find('"') else { continue };
let kind = &after[..end];
found += 1;
if !TYPES.contains(&format!("| '{kind}'")) {
missing.push(kind.to_owned());
}
}
assert!(
found >= 10,
"only {found} binding kinds were extracted from as_str — the parse is broken, so this \
test is asserting nothing"
);
assert!(
missing.is_empty(),
"BindingKind can return {missing:?}, which the union in index.d.ts does not include"
);
}