use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use std::str::FromStr as _;
use proc_macro2::{Delimiter, TokenStream, TokenTree};
pub const EDGE_GUARD_ATTRS: &[&str] = &["secured", "authorize", "step_up", "throttle", "intercept"];
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EdgeFn {
pub name: String,
pub file: String,
pub line: usize,
pub guards: Vec<String>,
pub module_path: Vec<String>,
pub crate_root: String,
}
impl EdgeFn {
#[must_use]
pub fn location(&self) -> String {
format!("{} @ {}:{}", self.name, self.file, self.line)
}
}
#[derive(Debug, Clone, Default)]
pub struct EdgeScan {
pub functions: Vec<EdgeFn>,
pub registered: BTreeSet<(String, String)>,
pub registrations: usize,
pub files_scanned: usize,
pub crate_name: Option<String>,
}
impl EdgeScan {
#[must_use]
pub const fn is_empty(&self) -> bool {
self.functions.is_empty()
}
#[must_use]
pub fn names(&self) -> Vec<&str> {
self.functions.iter().map(|f| f.name.as_str()).collect()
}
#[must_use]
pub fn unregistered(&self) -> Vec<&EdgeFn> {
self.functions
.iter()
.filter(|f| !is_registered(f, &self.registered, self.crate_name.as_deref()))
.collect()
}
#[must_use]
pub fn registered_fns(&self) -> Vec<&EdgeFn> {
self.functions
.iter()
.filter(|f| is_registered(f, &self.registered, self.crate_name.as_deref()))
.collect()
}
#[must_use]
pub fn guarded(&self) -> Vec<&EdgeFn> {
self.functions
.iter()
.filter(|f| !f.guards.is_empty())
.collect()
}
}
fn is_registered(
f: &EdgeFn,
registered: &BTreeSet<(String, String)>,
crate_name: Option<&str>,
) -> bool {
registered.iter().any(|(written_in, entry)| {
let (qualifier, name) = entry.rsplit_once("::").unwrap_or(("", entry.as_str()));
if name != f.name {
return false;
}
if qualifier.is_empty() {
return true;
}
let mut qualifier_segments = qualifier.split("::");
match qualifier_segments.clone().next() {
Some("crate") => {
if *written_in != f.crate_root {
return false;
}
qualifier_segments.next();
}
Some(seg) if crate_name.is_some_and(|name| name == strip_raw_prefix(seg)) => {
if !f.crate_root.is_empty() {
return false;
}
qualifier_segments.next();
}
_ => {
if *written_in != f.crate_root {
return false;
}
}
}
qualifier_segments.map(strip_raw_prefix).eq(f
.module_path
.iter()
.map(|segment| strip_raw_prefix(segment)))
})
}
fn strip_raw_prefix(segment: &str) -> &str {
segment.strip_prefix("r#").unwrap_or(segment)
}
#[cfg(test)]
fn scan_sources_with_features(
sources: &[(&str, &str)],
default_features: &BTreeSet<String>,
) -> EdgeScan {
let mut scan = EdgeScan::default();
for (file, src) in sources {
scan_source(file, src, None, default_features, &mut scan);
scan.files_scanned += 1;
}
scan
}
#[cfg(test)]
#[must_use]
pub fn scan_sources(sources: &[(&str, &str)]) -> EdgeScan {
scan_sources_with_features(sources, &BTreeSet::new())
}
#[cfg(test)]
#[must_use]
fn resolve_edge_scan(project_root: &Path) -> EdgeScan {
resolve_edge_scan_with_extra_file(project_root, &[], None)
}
fn ordinary_walk_sources(
files: &[PathBuf],
project_root: &Path,
claimed: &BTreeSet<PathBuf>,
capsule_claimed: &BTreeSet<PathBuf>,
capsule_aliased: &BTreeSet<PathBuf>,
) -> Vec<(String, String)> {
files
.iter()
.filter_map(|path| {
let rel = path
.strip_prefix(project_root)
.unwrap_or(path)
.to_string_lossy()
.replace('\\', "/");
if claimed.contains(path.as_path()) {
return None;
}
if capsule_claimed.contains(path.as_path()) && !capsule_aliased.contains(path.as_path())
{
return None;
}
let src = std::fs::read_to_string(path).ok()?;
Some((rel, src))
})
.collect()
}
fn scan_ordinary_sources(
sources: &[(String, String)],
project_root: &Path,
table: Option<&toml::Table>,
path_overrides: &BTreeMap<String, Vec<(String, Vec<String>)>>,
capsule_aliased: &BTreeSet<PathBuf>,
default_features: &BTreeSet<String>,
scan: &mut EdgeScan,
) {
let capsule_aliased_rel: BTreeSet<String> = capsule_aliased
.iter()
.map(|path| {
path.strip_prefix(project_root)
.unwrap_or(path)
.to_string_lossy()
.replace('\\', "/")
})
.collect();
for (rel, src) in sources {
let path_override = if capsule_aliased_rel.contains(rel.as_str()) {
None
} else {
path_overrides.get(rel.as_str())
};
if let Some(identities) = path_override {
for (crate_root, module_path) in identities {
scan_source_with_context(
rel,
src,
crate_root,
module_path.clone(),
default_features,
scan,
);
}
} else {
scan_source(rel, src, table, default_features, scan);
}
}
}
fn resolve_edge_scan_impl(
project_root: &Path,
requested_features: &[&str],
capsule_bin_file: Option<&Path>,
) -> EdgeScan {
let manifest = std::fs::read_to_string(project_root.join("Cargo.toml")).ok();
let table = manifest
.as_deref()
.and_then(|manifest| toml::from_str::<toml::Table>(manifest).ok());
let resolver_v1 = resolver_v1_is_in_effect(project_root, table.as_ref());
let default_features = manifest
.as_deref()
.map(|manifest| {
enabled_features_from_manifest_for_resolver(manifest, requested_features, resolver_v1)
})
.unwrap_or_default();
let crate_name = table.as_ref().and_then(rust_crate_name_from_manifest);
let custom_lib_path = table.as_ref().and_then(custom_lib_path_from_manifest);
let mut scan = EdgeScan {
crate_name,
..EdgeScan::default()
};
let mut claimed: BTreeSet<PathBuf> = BTreeSet::new();
let mut capsule_claimed: BTreeSet<PathBuf> = BTreeSet::new();
if let Some(lib_path) = &custom_lib_path {
let lib_file = project_root.join(lib_path);
if lib_file.is_file() {
let (touched, _aliased) = scan_bin_crate_tree(
&lib_file,
project_root,
"",
Vec::new(),
&default_features,
&mut scan,
);
claimed.extend(touched);
}
}
let mut capsule_aliased: BTreeSet<PathBuf> = BTreeSet::new();
if let Some(file) = capsule_bin_file {
let (touched, aliased) = scan_bin_crate_tree(
file,
project_root,
"bin:edge-capsule",
Vec::new(),
&default_features,
&mut scan,
);
capsule_claimed.extend(touched);
capsule_aliased.extend(aliased);
}
let mut files = Vec::new();
collect_rs_files(&project_root.join("src"), &mut files);
if let Some(lib_path) = &custom_lib_path {
let conventional_lib_file = project_root.join("src/lib.rs");
let custom_lib_file = lexically_normalize_path(&project_root.join(lib_path));
if custom_lib_file != conventional_lib_file {
files.retain(|path| path != &conventional_lib_file);
}
}
files.sort();
let path_overrides =
path_attribute_module_paths(project_root, table.as_ref(), &default_features);
let sources: Vec<(String, String)> = ordinary_walk_sources(
&files,
project_root,
&claimed,
&capsule_claimed,
&capsule_aliased,
);
let sources_rel: BTreeSet<&str> = sources.iter().map(|(rel, _)| rel.as_str()).collect();
scan_ordinary_sources(
&sources,
project_root,
table.as_ref(),
&path_overrides,
&capsule_aliased,
&default_features,
&mut scan,
);
scan.files_scanned += sources.len();
for (target_rel, identities) in &path_overrides {
if sources_rel.contains(target_rel.as_str()) {
continue;
}
let target_path = project_root.join(target_rel);
if claimed.contains(&target_path) {
continue;
}
for (crate_root, module_path) in identities {
let (touched, _aliased) = scan_bin_crate_tree(
&target_path,
project_root,
crate_root,
module_path.clone(),
&default_features,
&mut scan,
);
claimed.extend(touched);
}
}
scan
}
#[must_use]
fn custom_lib_path_from_manifest(table: &toml::Table) -> Option<String> {
table
.get("lib")
.and_then(|lib| lib.get("path"))
.and_then(toml::Value::as_str)
.map(str::to_owned)
}
#[must_use]
pub fn resolve_edge_scan_with_extra_file(
project_root: &Path,
requested_features: &[&str],
extra_file: Option<&Path>,
) -> EdgeScan {
resolve_edge_scan_impl(project_root, requested_features, extra_file)
}
#[must_use]
fn package_name_from_manifest(table: &toml::Table) -> Option<String> {
table
.get("package")
.and_then(|package| package.get("name"))
.and_then(toml::Value::as_str)
.map(str::to_owned)
}
#[must_use]
fn target_key_matches_edge_capsule(target_key: &str) -> bool {
if target_key == crate::build::EDGE_TARGET {
return true;
}
let Some(inner) = target_key
.strip_prefix("cfg(")
.and_then(|rest| rest.strip_suffix(')'))
else {
return false;
};
syn::parse_str::<TargetCfgPredicate>(inner).is_ok_and(|pred| pred.eval())
}
enum TargetCfgPredicate {
Leaf(bool),
Not(Box<Self>),
All(Vec<Self>),
Any(Vec<Self>),
}
const WASM32_WASIP1_CFG_VALUES: &[(&str, &str)] = &[
("target_arch", "wasm32"),
("target_os", "wasi"),
("target_family", "wasm"),
("target_env", "p1"),
("target_pointer_width", "32"),
("target_vendor", "unknown"),
("target_endian", "little"),
("target_abi", ""),
("target_has_atomic", "8"),
("target_has_atomic", "16"),
("target_has_atomic", "32"),
("target_has_atomic", "64"),
("target_has_atomic", "ptr"),
("panic", "abort"),
];
impl syn::parse::Parse for TargetCfgPredicate {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let ident: syn::Ident = syn::ext::IdentExt::parse_any(input)?;
if ident == "true" || ident == "false" {
return Ok(Self::Leaf(ident == "true"));
}
if ident == "target_feature" && input.peek(syn::Token![=]) {
input.parse::<syn::Token![=]>()?;
let _lit: syn::LitStr = input.parse()?;
return Ok(Self::Leaf(false));
}
let key_is_known = WASM32_WASIP1_CFG_VALUES.iter().any(|(key, _)| ident == key);
if key_is_known {
if input.peek(syn::Token![=]) {
input.parse::<syn::Token![=]>()?;
let lit: syn::LitStr = input.parse()?;
let value = lit.value();
let matches = WASM32_WASIP1_CFG_VALUES
.iter()
.any(|(key, known_value)| ident == key && value == *known_value);
return Ok(Self::Leaf(matches));
}
return Ok(Self::Leaf(false));
}
if ident == "not" {
let content;
syn::parenthesized!(content in input);
let inner: Self = content.parse()?;
return Ok(Self::Not(Box::new(inner)));
}
if ident == "all" || ident == "any" {
let content;
syn::parenthesized!(content in input);
let list =
syn::punctuated::Punctuated::<Self, syn::Token![,]>::parse_terminated(&content)?;
let parts: Vec<Self> = list.into_iter().collect();
return Ok(if ident == "all" {
Self::All(parts)
} else {
Self::Any(parts)
});
}
if ident == "windows" || ident == "unix" {
return Ok(Self::Leaf(false));
}
if ident == "debug_assertions" {
return Ok(Self::Leaf(true));
}
if !input.peek(syn::Token![=]) {
return Ok(Self::Leaf(false));
}
input.parse::<syn::Token![=]>()?;
input.parse::<syn::LitStr>()?;
Ok(Self::Leaf(false))
}
}
impl TargetCfgPredicate {
fn eval(&self) -> bool {
match self {
Self::Leaf(matches) => *matches,
Self::Not(inner) => !inner.eval(),
Self::All(parts) => parts.iter().all(Self::eval),
Self::Any(parts) => parts.iter().any(Self::eval),
}
}
}
fn resolver_v1_is_in_effect(project_root: &Path, table: Option<&toml::Table>) -> bool {
if let Some(t) = table
&& t.contains_key("workspace")
{
return workspace_implies_resolver_v1(t);
}
if let Some(workspace_table) = find_ancestor_workspace_manifest(project_root) {
return workspace_implies_resolver_v1(&workspace_table);
}
let Some(package) = table
.and_then(|t| t.get("package"))
.and_then(toml::Value::as_table)
else {
return true;
};
if let Some(resolver) = package.get("resolver").and_then(toml::Value::as_str) {
return resolver == "1";
}
edition_implies_resolver_v1(package)
}
fn workspace_implies_resolver_v1(table: &toml::Table) -> bool {
let workspace = table.get("workspace").and_then(toml::Value::as_table);
if let Some(resolver) = workspace
.and_then(|w| w.get("resolver"))
.and_then(toml::Value::as_str)
{
return resolver == "1";
}
let Some(root_package) = table.get("package").and_then(toml::Value::as_table) else {
return true; };
root_package_edition_implies_resolver_v1(root_package, workspace)
}
fn edition_implies_resolver_v1(package_table: &toml::Table) -> bool {
!matches!(
package_table.get("edition").and_then(toml::Value::as_str),
Some("2021" | "2024")
)
}
fn root_package_edition_implies_resolver_v1(
root_package: &toml::Table,
workspace: Option<&toml::Table>,
) -> bool {
match root_package.get("edition") {
Some(toml::Value::String(edition)) => !matches!(edition.as_str(), "2021" | "2024"),
Some(toml::Value::Table(inherit))
if inherit.get("workspace").and_then(toml::Value::as_bool) == Some(true) =>
{
let inherited_edition = workspace
.and_then(|w| w.get("package"))
.and_then(toml::Value::as_table)
.and_then(|p| p.get("edition"))
.and_then(toml::Value::as_str);
!matches!(inherited_edition, Some("2021" | "2024"))
}
_ => true, }
}
fn find_ancestor_workspace_manifest(project_root: &Path) -> Option<toml::Table> {
let mut dir = project_root.parent();
while let Some(candidate) = dir {
if let Ok(content) = std::fs::read_to_string(candidate.join("Cargo.toml"))
&& let Ok(table) = toml::from_str::<toml::Table>(&content)
&& table.contains_key("workspace")
{
let workspace_globs = |key: &str| -> Vec<String> {
table
.get("workspace")
.and_then(toml::Value::as_table)
.and_then(|w| w.get(key))
.and_then(toml::Value::as_array)
.map(|entries| {
entries
.iter()
.filter_map(toml::Value::as_str)
.map(str::to_owned)
.collect()
})
.unwrap_or_default()
};
let governed = project_root.strip_prefix(candidate).is_ok_and(|rel| {
let rel = rel.to_string_lossy().replace('\\', "/");
let members = workspace_globs("members");
let member_patterns: Vec<&str> = members.iter().map(String::as_str).collect();
if any_glob_matches(&member_patterns, &rel, false) {
return true;
}
let exclude = workspace_globs("exclude");
let exclude_patterns: Vec<&str> = exclude.iter().map(String::as_str).collect();
!any_glob_matches(&exclude_patterns, &rel, true)
});
if governed {
return Some(table);
}
}
dir = candidate.parent();
}
None
}
fn any_glob_matches(patterns: &[&str], rel: &str, allow_subtree_match: bool) -> bool {
let rel_segments: Vec<&str> = rel.split('/').collect();
patterns.iter().any(|pattern| {
let pattern_segments: Vec<&str> = pattern.split('/').collect();
let length_matches = if allow_subtree_match {
pattern_segments.len() <= rel_segments.len()
} else {
pattern_segments.len() == rel_segments.len()
};
length_matches
&& pattern_segments
.iter()
.zip(&rel_segments)
.all(|(pattern, segment)| segment_glob_matches(pattern, segment))
})
}
fn segment_glob_matches(pattern: &str, segment: &str) -> bool {
pattern
.split_once('*')
.map_or(pattern == segment, |(prefix, suffix)| {
segment.len() >= prefix.len() + suffix.len()
&& segment.starts_with(prefix)
&& segment.ends_with(suffix)
})
}
#[must_use]
fn is_optional_dependency(table: &toml::Table, name: &str, resolver_v1: bool) -> bool {
let declares_optional = |deps: &toml::Table| {
deps.get(name)
.and_then(toml::Value::as_table)
.and_then(|dep| dep.get("optional"))
.and_then(toml::Value::as_bool)
== Some(true)
};
let in_table = |key: &str, tbl: &toml::Table| {
tbl.get(key)
.and_then(toml::Value::as_table)
.is_some_and(declares_optional)
};
if in_table("dependencies", table) {
return true;
}
table
.get("target")
.and_then(toml::Value::as_table)
.is_some_and(|targets| {
targets
.iter()
.filter(|(target_key, _)| {
resolver_v1 || target_key_matches_edge_capsule(target_key)
})
.map(|(_, target)| target)
.filter_map(toml::Value::as_table)
.any(|target| in_table("dependencies", target))
})
}
#[must_use]
fn implicit_feature_is_suppressed(features_table: Option<&toml::Table>, pkg: &str) -> bool {
let needle = format!("dep:{pkg}");
features_table.is_some_and(|features| {
features.values().any(|entries| {
entries
.as_array()
.is_some_and(|entries| entries.iter().any(|entry| entry.as_str() == Some(&needle)))
})
})
}
#[must_use]
fn has_explicit_feature_entry(features_table: Option<&toml::Table>, pkg: &str) -> bool {
features_table.is_some_and(|features| features.contains_key(pkg))
}
#[must_use]
fn rust_crate_name_from_manifest(table: &toml::Table) -> Option<String> {
table
.get("lib")
.and_then(|lib| lib.get("name"))
.and_then(toml::Value::as_str)
.map(str::to_owned)
.or_else(|| package_name_from_manifest(table).map(|name| name.replace('-', "_")))
}
#[cfg(test)]
#[must_use]
fn enabled_features_from_manifest(manifest: &str, requested: &[&str]) -> BTreeSet<String> {
enabled_features_from_manifest_for_resolver(manifest, requested, false)
}
#[must_use]
fn enabled_features_from_manifest_for_resolver(
manifest: &str,
requested: &[&str],
resolver_v1: bool,
) -> BTreeSet<String> {
let mut enabled = BTreeSet::new();
let table = toml::from_str::<toml::Table>(manifest).ok();
let features_table = table
.as_ref()
.and_then(|table| table.get("features"))
.and_then(toml::Value::as_table)
.cloned();
let package_name = table.as_ref().and_then(package_name_from_manifest);
let default_members = features_table
.as_ref()
.and_then(|features| features.get("default"))
.and_then(toml::Value::as_array);
let mut queue: Vec<String> = Vec::new();
if let Some(members) = default_members {
queue.push("default".to_owned());
queue.extend(
members
.iter()
.filter_map(toml::Value::as_str)
.map(str::to_owned),
);
}
queue.extend(requested.iter().map(|name| {
name.split_once('/').map_or_else(
|| (*name).to_owned(),
|(pkg, feat)| {
if Some(pkg.trim_end_matches('?')) == package_name.as_deref() {
feat.to_owned()
} else {
(*name).to_owned()
}
},
)
}));
while let Some(name) = queue.pop() {
if name.starts_with("dep:") {
continue;
}
if let Some((pkg, _feat)) = name.split_once('/') {
if !pkg.ends_with('?')
&& table
.as_ref()
.is_some_and(|t| is_optional_dependency(t, pkg, resolver_v1))
&& (has_explicit_feature_entry(features_table.as_ref(), pkg)
|| !implicit_feature_is_suppressed(features_table.as_ref(), pkg))
{
queue.push(pkg.to_owned());
}
continue;
}
if !enabled.insert(name.clone()) {
continue; }
if let Some(sub) = features_table
.as_ref()
.and_then(|features| features.get(&name))
.and_then(toml::Value::as_array)
{
queue.extend(
sub.iter()
.filter_map(toml::Value::as_str)
.map(str::to_owned),
);
}
}
enabled
}
fn collect_rs_files(dir: &Path, out: &mut Vec<PathBuf>) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let Ok(file_type) = entry.file_type() else {
continue;
};
if file_type.is_symlink() {
continue;
}
let path = entry.path();
let name = entry.file_name();
let name = name.to_string_lossy();
if file_type.is_dir() {
if name == "target" || name == "tests" || name.starts_with('.') {
continue;
}
collect_rs_files(&path, out);
} else if file_type.is_file() && path.extension().and_then(|s| s.to_str()) == Some("rs") {
out.push(path);
}
}
}
fn scan_source(
file: &str,
src: &str,
table: Option<&toml::Table>,
default_features: &BTreeSet<String>,
scan: &mut EdgeScan,
) {
let (crate_root, module_path) = crate_context_from_file(file, table);
scan_source_with_context(file, src, &crate_root, module_path, default_features, scan);
}
fn scan_source_with_context(
file: &str,
src: &str,
crate_root: &str,
module_path: Vec<String>,
default_features: &BTreeSet<String>,
scan: &mut EdgeScan,
) {
if let Ok(ast) = syn::parse_file(src) {
let mut module_path = module_path.clone();
scan_items(
&ast.items,
file,
crate_root,
&mut module_path,
default_features,
scan,
);
}
if let Ok(stream) = TokenStream::from_str(src) {
let mut module_path = module_path;
collect_registrations(
&stream,
crate_root,
&mut module_path,
default_features,
scan,
);
}
}
#[must_use]
fn crate_context_from_file(file: &str, table: Option<&toml::Table>) -> (String, Vec<String>) {
let without_ext = file.strip_suffix(".rs").unwrap_or(file);
let Some(without_src) = without_ext.strip_prefix("src/") else {
return (format!("bin:{without_ext}"), Vec::new());
};
if without_src.is_empty() {
return (String::new(), Vec::new());
}
if let Some(rest) = without_src.strip_prefix("bin/") {
let (name, bin_relative) = rest.split_once('/').unwrap_or((rest, ""));
if src_bin_target_is_real(table, name) {
return (
format!("bin:{name}"),
module_path_from_segments(bin_relative),
);
}
}
if without_src == "main" {
if src_main_is_real(table) {
return ("bin:main".to_owned(), Vec::new());
}
}
(String::new(), module_path_from_segments(without_src))
}
#[must_use]
fn src_bin_target_is_real(table: Option<&toml::Table>, name: &str) -> bool {
let autobins_disabled = table
.and_then(|t| t.get("package"))
.and_then(|p| p.get("autobins"))
.and_then(toml::Value::as_bool)
== Some(false);
if !autobins_disabled {
return true;
}
let Some(bins) = table
.and_then(|t| t.get("bin"))
.and_then(toml::Value::as_array)
else {
return false;
};
bins.iter().any(|bin| {
bin.get("path").and_then(toml::Value::as_str).map_or_else(
|| bin.get("name").and_then(toml::Value::as_str) == Some(name),
|path| {
let normalized = path.strip_prefix("./").unwrap_or(path);
normalized == format!("src/bin/{name}.rs")
|| normalized == format!("src/bin/{name}/main.rs")
},
)
})
}
#[must_use]
fn src_main_is_real(table: Option<&toml::Table>) -> bool {
let autobins_disabled = table
.and_then(|t| t.get("package"))
.and_then(|p| p.get("autobins"))
.and_then(toml::Value::as_bool)
== Some(false);
if !autobins_disabled {
return true;
}
let Some(bins) = table
.and_then(|t| t.get("bin"))
.and_then(toml::Value::as_array)
else {
return false;
};
bins.iter().any(|bin| {
bin.get("path")
.and_then(toml::Value::as_str)
.is_some_and(|path| path.strip_prefix("./").unwrap_or(path) == "src/main.rs")
})
}
fn module_path_from_segments(path: &str) -> Vec<String> {
if path.is_empty() {
return Vec::new();
}
let mut segments: Vec<&str> = path.split('/').collect();
match segments.last() {
Some(&"mod") => {
segments.pop();
}
Some(&("main" | "lib")) if segments.len() == 1 => {
segments.pop();
}
_ => {}
}
segments.into_iter().map(str::to_owned).collect()
}
fn scan_items(
items: &[syn::Item],
file: &str,
crate_root: &str,
module_path: &mut Vec<String>,
default_features: &BTreeSet<String>,
scan: &mut EdgeScan,
) {
for item in items {
match item {
syn::Item::Fn(item_fn) => {
if let Some(found) = edge_fn(
&item_fn.attrs,
&item_fn.sig,
file,
crate_root,
module_path,
default_features,
) {
scan.functions.push(found);
}
}
syn::Item::Mod(item_mod) => {
if let Some((_, inner)) = &item_mod.content {
let cfg_excludes = item_mod
.attrs
.iter()
.any(|attr| attr_cfg_excludes(attr, default_features));
if cfg_excludes {
continue;
}
module_path.push(item_mod.ident.to_string());
scan_items(inner, file, crate_root, module_path, default_features, scan);
module_path.pop();
}
}
_ => {}
}
}
}
fn out_of_line_mod_declarations(
items: &[syn::Item],
default_features: &BTreeSet<String>,
) -> Vec<(Vec<String>, String, Option<String>)> {
let mut out = Vec::new();
let mut path = Vec::new();
collect_out_of_line_mods(items, default_features, &mut path, &mut out);
out
}
fn collect_out_of_line_mods(
items: &[syn::Item],
default_features: &BTreeSet<String>,
path: &mut Vec<String>,
out: &mut Vec<(Vec<String>, String, Option<String>)>,
) {
for item in items {
let syn::Item::Mod(item_mod) = item else {
continue;
};
let cfg_excludes = item_mod
.attrs
.iter()
.any(|attr| attr_cfg_excludes(attr, default_features));
if cfg_excludes {
continue;
}
match &item_mod.content {
Some((_, inner)) => {
path.push(item_mod.ident.to_string());
collect_out_of_line_mods(inner, default_features, path, out);
path.pop();
}
None => out.push((
path.clone(),
item_mod.ident.to_string(),
path_attribute_value(&item_mod.attrs, default_features),
)),
}
}
}
fn path_attribute_module_paths(
project_root: &Path,
table: Option<&toml::Table>,
default_features: &BTreeSet<String>,
) -> BTreeMap<String, Vec<(String, Vec<String>)>> {
let mut files = Vec::new();
collect_rs_files(&project_root.join("src"), &mut files);
files.sort();
let mut overrides: BTreeMap<String, Vec<(String, Vec<String>)>> = BTreeMap::new();
let mut conventional_declarations: Vec<(String, String, Vec<String>)> = Vec::new();
for declaring_file in &files {
let Ok(src) = std::fs::read_to_string(declaring_file) else {
continue;
};
let Ok(ast) = syn::parse_file(&src) else {
continue;
};
let declaring_rel = declaring_file
.strip_prefix(project_root)
.unwrap_or(declaring_file)
.to_string_lossy()
.replace('\\', "/");
let (crate_root, base_module_path) = crate_context_from_file(&declaring_rel, table);
let found = out_of_line_mod_declarations(&ast.items, default_features);
for (inline, name, path_value) in found {
let mut module_path = base_module_path.clone();
module_path.extend(inline.iter().cloned());
module_path.push(name.clone());
if let Some(path_value) = path_value {
let inline_dir = base_module_path
.iter()
.chain(inline.iter())
.fold(project_root.join("src"), |dir, segment| dir.join(segment));
let target_file = lexically_normalize_path(&inline_dir.join(&path_value));
let target_rel = target_file
.strip_prefix(project_root)
.unwrap_or(&target_file)
.to_string_lossy()
.replace('\\', "/");
overrides
.entry(target_rel)
.or_default()
.push((crate_root.clone(), module_path));
} else if let Some(target_file) = resolve_out_of_line_module_file(
&project_root.join("src"),
&{
let mut dir_segments = base_module_path.clone();
dir_segments.extend(inline);
dir_segments
},
&name,
) {
let target_rel = target_file
.strip_prefix(project_root)
.unwrap_or(&target_file)
.to_string_lossy()
.replace('\\', "/");
conventional_declarations.push((target_rel, crate_root.clone(), module_path));
}
}
}
for (target_rel, crate_root, module_path) in conventional_declarations {
if let Some(identities) = overrides.get_mut(&target_rel)
&& !identities.contains(&(crate_root.clone(), module_path.clone()))
{
identities.push((crate_root, module_path));
}
}
overrides
}
fn path_name_value_str(meta: &syn::Meta) -> Option<String> {
if !meta.path().is_ident("path") {
return None;
}
let syn::Meta::NameValue(name_value) = meta else {
return None;
};
let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(lit_str),
..
}) = &name_value.value
else {
return None;
};
Some(lit_str.value())
}
fn path_attribute_value(
attrs: &[syn::Attribute],
default_features: &BTreeSet<String>,
) -> Option<String> {
attrs.iter().find_map(|attr| {
if let Some(value) = path_name_value_str(&attr.meta) {
return Some(value);
}
if attr.path().is_ident("cfg_attr")
&& let syn::Meta::List(list) = &attr.meta
{
return path_value_from_cfg_attr_metas(list, default_features);
}
None
})
}
fn path_value_from_cfg_attr_metas(
list: &syn::MetaList,
default_features: &BTreeSet<String>,
) -> Option<String> {
let (condition, metas) = cfg_attr_payload(list, default_features)?;
if condition != Some(true) {
return None;
}
metas.iter().find_map(|meta| {
if let Some(value) = path_name_value_str(meta) {
return Some(value);
}
if meta.path().is_ident("cfg_attr")
&& let syn::Meta::List(inner) = meta
{
return path_value_from_cfg_attr_metas(inner, default_features);
}
None
})
}
fn resolve_out_of_line_module_file(
root_dir: &Path,
dir_segments: &[String],
name: &str,
) -> Option<PathBuf> {
fn strip_raw(segment: &str) -> &str {
segment.strip_prefix("r#").unwrap_or(segment)
}
let dir = dir_segments
.iter()
.fold(root_dir.to_path_buf(), |dir, segment| {
dir.join(strip_raw(segment))
});
let name = strip_raw(name);
let flat = dir.join(format!("{name}.rs"));
if flat.is_file() {
return Some(flat);
}
let nested = dir.join(name).join("mod.rs");
if nested.is_file() {
return Some(nested);
}
None
}
fn lexically_normalize_path(path: &Path) -> PathBuf {
let mut out = PathBuf::new();
for component in path.components() {
match component {
std::path::Component::CurDir => {}
std::path::Component::ParentDir
if matches!(
out.components().next_back(),
Some(std::path::Component::Normal(_))
) =>
{
out.pop();
}
other => out.push(other),
}
}
out
}
fn scan_bin_crate_tree(
root_file: &Path,
project_root: &Path,
crate_root: &str,
initial_module_path: Vec<String>,
default_features: &BTreeSet<String>,
scan: &mut EdgeScan,
) -> (BTreeSet<PathBuf>, BTreeSet<PathBuf>) {
let root_file = lexically_normalize_path(root_file);
let root_file = root_file.as_path();
let root_dir = root_file.parent().unwrap_or_else(|| Path::new(""));
let mut scanned_identities: BTreeSet<(PathBuf, Vec<String>)> = BTreeSet::new();
let mut aliased: BTreeSet<PathBuf> = BTreeSet::new();
let mut queue: std::collections::VecDeque<(PathBuf, Vec<String>, Vec<PathBuf>, PathBuf)> =
std::collections::VecDeque::new();
queue.push_back((
root_file.to_path_buf(),
initial_module_path,
vec![root_file.to_path_buf()],
root_dir.to_path_buf(),
));
while let Some((file, module_path, ancestors, resolution_base)) = queue.pop_front() {
if !scanned_identities.insert((file.clone(), module_path.clone())) {
continue; }
let Ok(src) = std::fs::read_to_string(&file) else {
continue;
};
let rel = file
.strip_prefix(project_root)
.unwrap_or(&file)
.to_string_lossy()
.replace('\\', "/");
scan_source_with_context(
&rel,
&src,
crate_root,
module_path.clone(),
default_features,
scan,
);
scan.files_scanned += 1;
let Ok(ast) = syn::parse_file(&src) else {
continue;
};
let declaring_dir = file.parent().unwrap_or_else(|| Path::new(""));
for (nested, name, path_value) in out_of_line_mod_declarations(&ast.items, default_features)
{
let mut dir_segments = module_path.clone();
dir_segments.extend(nested.iter().cloned());
let (resolved, child_resolution_base, is_aliased) = path_value.map_or_else(
|| {
let base = nested
.iter()
.fold(resolution_base.clone(), |dir, segment| dir.join(segment));
let resolved = resolve_out_of_line_module_file(&base, &[], &name);
let child_base = base.join(&name);
(resolved, child_base, false)
},
|path_value| {
let dir = nested
.iter()
.fold(declaring_dir.to_path_buf(), |dir, segment| {
dir.join(segment)
});
let resolved = lexically_normalize_path(&dir.join(&path_value));
let child_base = resolved
.parent()
.map_or_else(|| dir.clone(), Path::to_path_buf);
(Some(resolved), child_base, true)
},
);
let Some(resolved) = resolved else {
continue;
};
if ancestors.contains(&resolved) {
continue; }
if is_aliased {
aliased.insert(resolved.clone());
}
let mut child_module_path = dir_segments;
child_module_path.push(name);
let mut child_ancestors = ancestors.clone();
child_ancestors.push(resolved.clone());
queue.push_back((
resolved,
child_module_path,
child_ancestors,
child_resolution_base,
));
}
}
let touched: BTreeSet<PathBuf> = scanned_identities
.into_iter()
.map(|(file, _)| file)
.collect();
(touched, aliased)
}
fn attr_name(attr: &syn::Attribute) -> Option<String> {
attr.path().segments.last().map(|s| s.ident.to_string())
}
fn attr_names_including_cfg_attr(
attrs: &[syn::Attribute],
default_features: &BTreeSet<String>,
) -> Vec<String> {
let mut names = Vec::new();
for attr in attrs {
let Some(name) = attr_name(attr) else {
continue;
};
if name == "cfg_attr"
&& let syn::Meta::List(list) = &attr.meta
{
names.extend(cfg_attr_active_marker_names(list, default_features));
} else {
names.push(name);
}
}
names
}
fn cfg_attr_payload(
list: &syn::MetaList,
default_features: &BTreeSet<String>,
) -> Option<(Option<bool>, Vec<syn::Meta>)> {
let tokens: Vec<TokenTree> = list.tokens.clone().into_iter().collect();
let comma_index = tokens
.iter()
.position(|t| matches!(t, TokenTree::Punct(p) if p.as_char() == ','))?;
let condition_tokens: proc_macro2::TokenStream =
tokens[..comma_index].iter().cloned().collect();
let condition = syn::parse2::<CfgPredicate>(condition_tokens)
.ok()
.map(|pred| pred.eval(default_features));
let rest_tokens: proc_macro2::TokenStream = tokens[comma_index + 1..].iter().cloned().collect();
let metas = syn::parse::Parser::parse2(
syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated,
rest_tokens,
)
.map(|metas| metas.into_iter().collect())
.unwrap_or_default();
Some((condition, metas))
}
fn cfg_attr_active_marker_names(
list: &syn::MetaList,
default_features: &BTreeSet<String>,
) -> Vec<String> {
let Some((condition, metas)) = cfg_attr_payload(list, default_features) else {
return Vec::new();
};
if condition == Some(false) {
return Vec::new();
}
let mut names = Vec::new();
for meta in &metas {
let Some(name) = meta.path().segments.last().map(|s| s.ident.to_string()) else {
continue;
};
if name == "cfg_attr" {
if let syn::Meta::List(inner) = meta {
names.extend(cfg_attr_active_marker_names(inner, default_features));
}
} else {
names.push(name);
}
}
names
}
fn cfg_attr_injects_a_false_cfg(list: &syn::MetaList, default_features: &BTreeSet<String>) -> bool {
let Some((condition, metas)) = cfg_attr_payload(list, default_features) else {
return false;
};
if condition != Some(true) {
return false;
}
metas.iter().any(|meta| {
let syn::Meta::List(inner) = meta else {
return false;
};
if inner.path.is_ident("cfg") {
return syn::parse2::<CfgPredicate>(inner.tokens.clone())
.is_ok_and(|pred| !pred.eval(default_features));
}
inner.path.is_ident("cfg_attr") && cfg_attr_injects_a_false_cfg(inner, default_features)
})
}
fn edge_fn(
attrs: &[syn::Attribute],
sig: &syn::Signature,
file: &str,
crate_root: &str,
module_path: &[String],
default_features: &BTreeSet<String>,
) -> Option<EdgeFn> {
let names = attr_names_including_cfg_attr(attrs, default_features);
if !names.iter().any(|n| n == "edge") {
return None;
}
let cfg_excludes = attrs
.iter()
.any(|attr| attr_cfg_excludes(attr, default_features));
if cfg_excludes {
return None;
}
let guards: Vec<String> = names
.into_iter()
.filter(|n| EDGE_GUARD_ATTRS.contains(&n.as_str()))
.collect();
Some(EdgeFn {
name: sig.ident.to_string(),
file: file.to_owned(),
line: sig.ident.span().start().line,
guards,
module_path: module_path.to_vec(),
crate_root: crate_root.to_owned(),
})
}
enum CfgPredicate {
Feature(String),
Bool(bool),
Not(Box<Self>),
All(Vec<Self>),
Any(Vec<Self>),
}
impl syn::parse::Parse for CfgPredicate {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let ident: syn::Ident = syn::ext::IdentExt::parse_any(input)?;
if ident == "feature" {
input.parse::<syn::Token![=]>()?;
let lit: syn::LitStr = input.parse()?;
return Ok(Self::Feature(lit.value()));
}
if ident == "true" || ident == "false" {
return Ok(Self::Bool(ident == "true"));
}
if ident == "not" {
let content;
syn::parenthesized!(content in input);
let inner: Self = content.parse()?;
return Ok(Self::Not(Box::new(inner)));
}
if ident == "all" || ident == "any" {
let content;
syn::parenthesized!(content in input);
let list =
syn::punctuated::Punctuated::<Self, syn::Token![,]>::parse_terminated(&content)?;
let parts: Vec<Self> = list.into_iter().collect();
return Ok(if ident == "all" {
Self::All(parts)
} else {
Self::Any(parts)
});
}
Err(input.error("cfg predicate not resolvable by this scan"))
}
}
impl CfgPredicate {
fn eval(&self, default_features: &BTreeSet<String>) -> bool {
match self {
Self::Feature(name) => default_features.contains(name),
Self::Bool(value) => *value,
Self::Not(inner) => !inner.eval(default_features),
Self::All(parts) => parts.iter().all(|p| p.eval(default_features)),
Self::Any(parts) => parts.iter().any(|p| p.eval(default_features)),
}
}
}
fn eval_cfg_attr(attr: &syn::Attribute, default_features: &BTreeSet<String>) -> Option<bool> {
if !attr.path().is_ident("cfg") {
return None;
}
let syn::Meta::List(list) = &attr.meta else {
return None;
};
syn::parse2::<CfgPredicate>(list.tokens.clone())
.ok()
.map(|pred| pred.eval(default_features))
}
fn attr_cfg_excludes(attr: &syn::Attribute, default_features: &BTreeSet<String>) -> bool {
if eval_cfg_attr(attr, default_features) == Some(false) {
return true;
}
let syn::Meta::List(list) = &attr.meta else {
return false;
};
attr.path().is_ident("cfg_attr") && cfg_attr_injects_a_false_cfg(list, default_features)
}
fn collect_registrations(
stream: &TokenStream,
crate_root: &str,
module_path: &mut Vec<String>,
default_features: &BTreeSet<String>,
scan: &mut EdgeScan,
) {
let trees: Vec<TokenTree> = stream.clone().into_iter().collect();
let mut index = 0;
while index < trees.len() {
if let Some(next_index) = skip_cfg_excluded_statement(&trees, index, default_features) {
index = next_index;
continue;
}
if let TokenTree::Ident(ident) = &trees[index]
&& ident == "mod"
&& let Some(TokenTree::Ident(name)) = trees.get(index + 1)
&& let Some(TokenTree::Group(group)) = trees.get(index + 2)
&& group.delimiter() == Delimiter::Brace
{
let attr_index = skip_back_over_fn_modifiers(&trees, index);
if !preceding_cfg_excludes(&trees, attr_index, default_features) {
module_path.push(name.to_string());
collect_registrations(
&group.stream(),
crate_root,
module_path,
default_features,
scan,
);
module_path.pop();
}
index += 3;
continue;
}
if let TokenTree::Ident(ident) = &trees[index]
&& ident == "impl"
&& let Some(body_index) = simple_impl_body_index(&trees, index)
{
let attr_index = skip_back_over_fn_modifiers(&trees, index);
if let TokenTree::Group(body) = &trees[body_index]
&& !preceding_cfg_excludes(&trees, attr_index, default_features)
{
collect_registrations(
&body.stream(),
crate_root,
module_path,
default_features,
scan,
);
}
index = body_index + 1;
continue;
}
if let TokenTree::Ident(ident) = &trees[index]
&& ident == "fn"
&& let Some(body_index) = simple_fn_body_index(&trees, index)
{
let attr_index = skip_back_over_fn_modifiers(&trees, index);
if let TokenTree::Group(body) = &trees[body_index]
&& !preceding_cfg_excludes(&trees, attr_index, default_features)
{
collect_registrations(
&body.stream(),
crate_root,
module_path,
default_features,
scan,
);
}
index = body_index + 1;
continue;
}
match &trees[index] {
TokenTree::Group(group) => {
if !preceding_cfg_excludes(&trees, index, default_features) {
collect_registrations(
&group.stream(),
crate_root,
module_path,
default_features,
scan,
);
}
}
TokenTree::Ident(ident) if ident == "edge_routes" => {
let bang = matches!(
trees.get(index + 1),
Some(TokenTree::Punct(p)) if p.as_char() == '!'
);
if bang
&& let Some(TokenTree::Group(group)) = trees.get(index + 2)
&& group.delimiter() != Delimiter::None
&& !preceding_cfg_excludes(&trees, index, default_features)
{
scan.registrations += 1;
for name in registered_idents(&group.stream()) {
for candidate in registration_candidates(&name, module_path) {
scan.registered.insert((crate_root.to_owned(), candidate));
}
}
}
}
_ => {}
}
index += 1;
}
}
fn skip_back_over_fn_modifiers(trees: &[TokenTree], fn_index: usize) -> usize {
let mut i = fn_index;
while let Some(prev) = i.checked_sub(1) {
match &trees[prev] {
TokenTree::Ident(ident)
if matches!(
ident.to_string().as_str(),
"pub" | "async" | "const" | "unsafe" | "extern"
) =>
{
i = prev;
}
TokenTree::Group(group) if group.delimiter() == Delimiter::Parenthesis => {
if prev >= 1 && matches!(&trees[prev - 1], TokenTree::Ident(id) if id == "pub") {
i = prev - 1;
} else {
break;
}
}
TokenTree::Literal(_) => {
if prev >= 1 && matches!(&trees[prev - 1], TokenTree::Ident(id) if id == "extern") {
i = prev - 1;
} else {
break;
}
}
_ => break,
}
}
i
}
fn skip_leading_cfg_attrs(
trees: &[TokenTree],
mut i: usize,
default_features: &BTreeSet<String>,
) -> (bool, usize) {
let mut excluded = false;
while let Some(TokenTree::Punct(hash)) = trees.get(i) {
if hash.as_char() != '#' {
break;
}
let Some(TokenTree::Group(bracket)) = trees.get(i + 1) else {
break;
};
if bracket.delimiter() != Delimiter::Bracket {
break;
}
if cfg_attribute_group_is_false(bracket, default_features) {
excluded = true;
}
i += 2;
}
(excluded, i)
}
fn skip_forward_over_item_modifiers(trees: &[TokenTree], mut i: usize) -> usize {
loop {
match trees.get(i) {
Some(TokenTree::Ident(ident))
if matches!(
ident.to_string().as_str(),
"pub" | "async" | "const" | "unsafe" | "extern"
) =>
{
i += 1;
}
Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Parenthesis => {
if i >= 1 && matches!(&trees[i - 1], TokenTree::Ident(id) if id == "pub") {
i += 1;
} else {
break;
}
}
Some(TokenTree::Literal(_)) => {
if i >= 1 && matches!(&trees[i - 1], TokenTree::Ident(id) if id == "extern") {
i += 1;
} else {
break;
}
}
_ => break,
}
}
i
}
fn is_specially_handled_item(trees: &[TokenTree], i: usize) -> bool {
matches!(
trees.get(skip_forward_over_item_modifiers(trees, i)),
Some(TokenTree::Ident(ident)) if matches!(ident.to_string().as_str(), "mod" | "fn" | "impl")
)
}
fn skip_cfg_excluded_statement(
trees: &[TokenTree],
index: usize,
default_features: &BTreeSet<String>,
) -> Option<usize> {
if !(matches!(&trees[index], TokenTree::Punct(p) if p.as_char() == '#')
&& matches!(trees.get(index + 1), Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::Bracket))
{
return None;
}
let (excluded, attrs_end) = skip_leading_cfg_attrs(trees, index, default_features);
if !excluded || is_specially_handled_item(trees, attrs_end) {
return None;
}
if matches!(trees.get(attrs_end), Some(TokenTree::Group(_))) {
return Some(attrs_end + 1);
}
if let Some(block_end) = statement_without_semicolon_end(trees, attrs_end) {
return Some(block_end + 1);
}
let matched_angles = matched_angle_bracket_positions(trees, attrs_end);
let mut i = attrs_end;
let mut angle_depth: u32 = 0;
while i < trees.len() {
match &trees[i] {
TokenTree::Punct(p) if p.as_char() == '-' => {
let is_arrow =
matches!(trees.get(i + 1), Some(TokenTree::Punct(p2)) if p2.as_char() == '>');
i += usize::from(is_arrow) + 1;
}
TokenTree::Punct(p) if p.as_char() == '<' && matched_angles.contains(&i) => {
angle_depth += 1;
i += 1;
}
TokenTree::Punct(p) if p.as_char() == '>' && matched_angles.contains(&i) => {
angle_depth = angle_depth.saturating_sub(1);
i += 1;
}
TokenTree::Punct(p) if p.as_char() == ';' => {
i += 1;
break;
}
TokenTree::Punct(p) if p.as_char() == ',' => {
i += 1;
if angle_depth == 0 {
break;
}
}
_ => i += 1,
}
}
Some(i)
}
fn matched_angle_bracket_positions(trees: &[TokenTree], start: usize) -> BTreeSet<usize> {
let mut matched = BTreeSet::new();
let mut stack: Vec<usize> = Vec::new();
let mut i = start;
while i < trees.len() {
match trees.get(i) {
Some(TokenTree::Punct(p))
if p.as_char() == '-'
&& matches!(trees.get(i + 1), Some(TokenTree::Punct(p2)) if p2.as_char() == '>') =>
{
i += 2;
continue;
}
Some(TokenTree::Punct(p))
if p.as_char() == '='
&& matches!(trees.get(i + 1), Some(TokenTree::Punct(p2)) if p2.as_char() == '>') =>
{
stack.clear();
i += 2;
continue;
}
Some(TokenTree::Punct(p)) if p.as_char() == '<' => stack.push(i),
Some(TokenTree::Punct(p)) if p.as_char() == '>' => {
if let Some(open) = stack.pop() {
matched.insert(open);
matched.insert(i);
}
}
_ => {}
}
i += 1;
}
matched
}
fn control_flow_block_end(trees: &[TokenTree], mut i: usize) -> Option<usize> {
let start = i;
loop {
match trees.get(i) {
Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::Brace => {
if i == start
&& matches!(trees.get(start.wrapping_sub(1)), Some(TokenTree::Ident(id)) if matches!(id.to_string().as_str(), "if" | "while" | "match"))
{
i += 1;
continue;
}
if i > 0
&& matches!(trees.get(i - 1), Some(TokenTree::Punct(p)) if p.as_char() == '!')
{
i += 1;
continue;
}
if i > 0 && matches!(trees.get(i - 1), Some(TokenTree::Ident(id)) if id == "async")
{
i += 1;
continue;
}
if i > 1
&& matches!(trees.get(i - 1), Some(TokenTree::Ident(id)) if id == "move")
&& matches!(trees.get(i - 2), Some(TokenTree::Ident(id)) if id == "async")
{
i += 1;
continue;
}
if i > 0 && matches!(trees.get(i - 1), Some(TokenTree::Ident(id)) if id == "const")
{
i += 1;
continue;
}
if i > start
&& matches!(trees.get(i - 1), Some(TokenTree::Ident(id)) if id == "unsafe")
{
i += 1;
continue;
}
if matches!(trees.get(i + 1), Some(TokenTree::Punct(p)) if p.as_char() == '=')
|| matches!(trees.get(i + 1), Some(TokenTree::Ident(id)) if id == "in")
|| matches!(trees.get(i + 1), Some(TokenTree::Punct(p)) if p.as_char() == '|')
{
i += 1;
continue;
}
break;
}
Some(_) => i += 1,
None => return None,
}
}
if matches!(trees.get(i + 1), Some(TokenTree::Ident(id)) if id == "else") {
return match trees.get(i + 2) {
Some(TokenTree::Ident(id)) if id == "if" => control_flow_block_end(trees, i + 3),
Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::Brace => Some(i + 2),
_ => Some(i),
};
}
Some(i)
}
fn simple_path_end(trees: &[TokenTree], i: usize) -> usize {
let mut end = i + 1;
while matches!(trees.get(end), Some(TokenTree::Punct(p)) if p.as_char() == ':')
&& matches!(trees.get(end + 1), Some(TokenTree::Punct(p)) if p.as_char() == ':')
&& matches!(trees.get(end + 2), Some(TokenTree::Ident(_)))
{
end += 3;
}
end
}
fn statement_without_semicolon_end(trees: &[TokenTree], i: usize) -> Option<usize> {
if let Some(TokenTree::Ident(ident)) = trees.get(i) {
match ident.to_string().as_str() {
"if" | "match" | "while" | "loop" | "for" | "unsafe" => {
return control_flow_block_end(trees, i + 1);
}
"const" => {
return matches!(trees.get(i + 1), Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::Brace)
.then_some(i + 1);
}
"macro_rules" => {
return braced_or_semicolon_item_end(trees, i + 1);
}
_ => {}
}
let path_end = simple_path_end(trees, i);
if matches!(trees.get(path_end), Some(TokenTree::Punct(p)) if p.as_char() == '!')
&& matches!(trees.get(path_end + 1), Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::Brace)
{
return Some(path_end + 1);
}
}
if matches!(trees.get(i), Some(TokenTree::Punct(p)) if p.as_char() == '\'')
&& matches!(trees.get(i + 1), Some(TokenTree::Ident(_)))
&& matches!(trees.get(i + 2), Some(TokenTree::Punct(p)) if p.as_char() == ':')
{
return match trees.get(i + 3) {
Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::Brace => Some(i + 3),
Some(TokenTree::Ident(ident))
if matches!(ident.to_string().as_str(), "loop" | "while" | "for") =>
{
control_flow_block_end(trees, i + 4)
}
_ => None,
};
}
let item_start = skip_forward_over_item_modifiers(trees, i);
if let Some(TokenTree::Ident(ident)) = trees.get(item_start)
&& matches!(
ident.to_string().as_str(),
"struct" | "enum" | "union" | "trait"
)
{
return braced_or_semicolon_item_end(trees, item_start + 1);
}
if matches!(trees.get(item_start), Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::Brace)
{
return Some(item_start);
}
None
}
fn braced_or_semicolon_item_end(trees: &[TokenTree], mut i: usize) -> Option<usize> {
let mut angle_depth: i32 = 0;
loop {
match trees.get(i) {
Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::Brace && angle_depth == 0 => {
return Some(i);
}
Some(TokenTree::Punct(p)) if p.as_char() == ';' && angle_depth == 0 => {
return Some(i);
}
Some(TokenTree::Punct(p))
if p.as_char() == '-'
&& matches!(trees.get(i + 1), Some(TokenTree::Punct(p2)) if p2.as_char() == '>') =>
{
i += 2;
}
Some(TokenTree::Punct(p)) if p.as_char() == '<' => {
angle_depth += 1;
i += 1;
}
Some(TokenTree::Punct(p)) if p.as_char() == '>' => {
angle_depth -= 1;
if angle_depth < 0 {
return None;
}
i += 1;
}
Some(
TokenTree::Ident(_)
| TokenTree::Punct(_)
| TokenTree::Literal(_)
| TokenTree::Group(_),
) => {
i += 1;
}
_ => return None,
}
}
}
fn simple_impl_body_index(trees: &[TokenTree], impl_index: usize) -> Option<usize> {
let mut i = impl_index + 1;
let mut angle_depth: i32 = 0;
loop {
match trees.get(i) {
Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::Brace && angle_depth == 0 => {
return Some(i);
}
Some(TokenTree::Punct(p))
if p.as_char() == '-'
&& matches!(trees.get(i + 1), Some(TokenTree::Punct(p2)) if p2.as_char() == '>') =>
{
i += 2;
}
Some(TokenTree::Punct(p)) if p.as_char() == '<' => {
angle_depth += 1;
i += 1;
}
Some(TokenTree::Punct(p)) if p.as_char() == '>' => {
angle_depth -= 1;
if angle_depth < 0 {
return None;
}
i += 1;
}
Some(
TokenTree::Ident(_)
| TokenTree::Punct(_)
| TokenTree::Literal(_)
| TokenTree::Group(_),
) => {
i += 1;
}
_ => return None,
}
}
}
fn simple_fn_body_index(trees: &[TokenTree], fn_index: usize) -> Option<usize> {
let mut i = fn_index + 1;
if !matches!(trees.get(i), Some(TokenTree::Ident(_))) {
return None;
}
i += 1;
if matches!(trees.get(i), Some(TokenTree::Punct(p)) if p.as_char() == '<') {
let mut depth: i32 = 0;
loop {
match trees.get(i) {
Some(TokenTree::Punct(p))
if p.as_char() == '-'
&& matches!(trees.get(i + 1), Some(TokenTree::Punct(p2)) if p2.as_char() == '>') =>
{
i += 2;
}
Some(TokenTree::Punct(p)) if p.as_char() == '<' => {
depth += 1;
i += 1;
}
Some(TokenTree::Punct(p)) if p.as_char() == '>' => {
depth -= 1;
i += 1;
if depth == 0 {
break;
}
}
Some(
TokenTree::Ident(_)
| TokenTree::Punct(_)
| TokenTree::Literal(_)
| TokenTree::Group(_),
) => {
i += 1;
}
_ => return None,
}
if depth < 0 {
return None;
}
}
}
if !matches!(trees.get(i), Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::Parenthesis)
{
return None;
}
i += 1;
if matches!(trees.get(i), Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::Brace) {
return Some(i);
}
let starts_return_type_or_where = matches!(trees.get(i), Some(TokenTree::Punct(p)) if p.as_char() == '-')
&& matches!(trees.get(i + 1), Some(TokenTree::Punct(p)) if p.as_char() == '>');
let starts_where_only = matches!(trees.get(i), Some(TokenTree::Ident(id)) if id == "where");
if starts_return_type_or_where || starts_where_only {
if starts_return_type_or_where {
i += 2;
}
let mut angle_depth: i32 = 0;
loop {
match trees.get(i) {
Some(TokenTree::Group(g))
if g.delimiter() == Delimiter::Brace && angle_depth == 0 =>
{
return Some(i);
}
Some(TokenTree::Punct(p))
if p.as_char() == '-'
&& matches!(trees.get(i + 1), Some(TokenTree::Punct(p2)) if p2.as_char() == '>') =>
{
i += 2;
}
Some(TokenTree::Punct(p)) if p.as_char() == '<' => {
angle_depth += 1;
i += 1;
}
Some(TokenTree::Punct(p)) if p.as_char() == '>' => {
angle_depth -= 1;
if angle_depth < 0 {
return None;
}
i += 1;
}
Some(TokenTree::Punct(p)) if p.as_char() == ';' && angle_depth == 0 => {
return None;
}
Some(
TokenTree::Ident(_)
| TokenTree::Punct(_)
| TokenTree::Literal(_)
| TokenTree::Group(_),
) => i += 1,
_ => return None,
}
}
}
None
}
fn preceding_cfg_excludes(
trees: &[TokenTree],
item_index: usize,
default_features: &BTreeSet<String>,
) -> bool {
let mut i = item_index;
while i >= 2 {
let Some(TokenTree::Group(bracket)) = trees.get(i - 1) else {
break;
};
if bracket.delimiter() != Delimiter::Bracket {
break;
}
let Some(TokenTree::Punct(hash)) = trees.get(i - 2) else {
break;
};
if hash.as_char() != '#' {
break;
}
if cfg_attribute_group_is_false(bracket, default_features) {
return true;
}
i -= 2;
}
false
}
fn cfg_attribute_group_is_false(
group: &proc_macro2::Group,
default_features: &BTreeSet<String>,
) -> bool {
let inner: Vec<TokenTree> = group.stream().into_iter().collect();
let (Some(TokenTree::Ident(ident)), Some(TokenTree::Group(payload))) =
(inner.first(), inner.get(1))
else {
return false;
};
if payload.delimiter() != Delimiter::Parenthesis {
return false;
}
if ident == "cfg" {
return syn::parse2::<CfgPredicate>(payload.stream())
.is_ok_and(|pred| !pred.eval(default_features));
}
if ident == "cfg_attr" {
return syn::parse2::<syn::Meta>(group.stream())
.ok()
.and_then(|meta| match meta {
syn::Meta::List(list) if list.path.is_ident("cfg_attr") => Some(list),
_ => None,
})
.is_some_and(|list| cfg_attr_injects_a_false_cfg(&list, default_features));
}
false
}
fn registration_candidates(entry: &str, module_path: &[String]) -> Vec<String> {
let entry = entry.strip_prefix("::").unwrap_or(entry);
let segments: Vec<&str> = entry.split("::").collect();
let (mut base, rest): (Vec<String>, &[&str]) = match segments.first().copied() {
Some("self") => (module_path.to_vec(), &segments[1..]),
Some("super") => {
let mut base = module_path.to_vec();
let mut rest: &[&str] = &segments;
while rest.first().copied() == Some("super") {
base.pop();
rest = &rest[1..];
}
(base, rest)
}
_ => return vec![entry.to_owned()],
};
if base.is_empty() {
base.push("crate".to_owned());
}
base.extend(rest.iter().map(|s| (*s).to_owned()));
vec![base.join("::")]
}
fn registered_idents(stream: &TokenStream) -> Vec<String> {
let mut out = Vec::new();
let mut current = String::new();
for tree in stream.clone() {
match tree {
TokenTree::Punct(p) if p.as_char() == ',' => {
if !current.is_empty() {
out.push(std::mem::take(&mut current));
}
}
TokenTree::Punct(p) if p.as_char() == ':' => current.push(':'),
TokenTree::Ident(ident) => current.push_str(&ident.to_string()),
_ => {}
}
}
if !current.is_empty() {
out.push(current);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn scan_one(src: &str) -> EdgeScan {
scan_sources(&[("src/lib.rs", src)])
}
fn scan_one_with_features(src: &str, features: &[&str]) -> EdgeScan {
let default_features: BTreeSet<String> = features.iter().map(|s| (*s).to_owned()).collect();
scan_sources_with_features(&[("src/lib.rs", src)], &default_features)
}
#[test]
fn bare_edge_attribute_is_found() {
let scan = scan_one(
r#"
#[get("/hello")]
#[edge]
pub async fn hello() -> &'static str { "hi" }
"#,
);
assert_eq!(scan.names(), vec!["hello"]);
assert_eq!(scan.functions[0].file, "src/lib.rs");
assert!(scan.functions[0].guards.is_empty());
assert!(!scan.is_empty());
}
#[test]
fn qualified_edge_paths_are_recognized() {
let scan = scan_sources(&[
("src/a.rs", "#[autumn_web::edge]\nfn a() {}"),
("src/b.rs", "#[macros::edge]\nfn b() {}"),
("src/c.rs", "#[edge(needs(kv))]\nfn c() {}"),
]);
assert_eq!(scan.names(), vec!["a", "b", "c"]);
assert_eq!(scan.files_scanned, 3);
}
#[test]
fn unmarked_functions_are_ignored() {
let scan = scan_one("#[get(\"/x\")]\nfn plain() {}\n#[edgy]\nfn other() {}");
assert!(scan.is_empty(), "{:?}", scan.functions);
}
#[test]
fn an_edge_marker_behind_an_active_cfg_attr_is_found() {
let scan = scan_one_with_features(
r#"
#[cfg_attr(feature = "edge-routes", edge)]
pub fn hello() {}
"#,
&["edge-routes"],
);
assert_eq!(scan.names(), vec!["hello"]);
}
#[test]
fn an_edge_marker_behind_an_inactive_cfg_attr_is_not_found() {
let scan = scan_one_with_features(
r#"
#[cfg_attr(feature = "edge-routes", edge)]
pub fn hello() {}
"#,
&[],
);
assert!(scan.is_empty(), "{:?}", scan.functions);
}
#[test]
fn an_edge_marker_behind_an_unresolvable_cfg_attr_condition_is_still_found() {
let scan = scan_one_with_features(
r#"
#[cfg_attr(target_os = "wasi", edge)]
pub fn hello() {}
"#,
&[],
);
assert_eq!(scan.names(), vec!["hello"]);
}
#[test]
fn a_guard_attribute_behind_an_active_cfg_attr_is_recorded() {
let scan = scan_one_with_features(
r#"
#[edge]
#[cfg_attr(feature = "strict-auth", secured)]
pub fn hello() {}
"#,
&["strict-auth"],
);
assert_eq!(scan.functions[0].guards, vec!["secured".to_owned()]);
}
#[test]
fn an_edge_fn_with_a_cfg_attr_injected_false_cfg_is_excluded() {
let scan = scan_one_with_features(
r#"
#[cfg_attr(feature = "outer", cfg(feature = "inner"))]
#[edge]
pub fn hello() {}
"#,
&["outer"],
);
assert!(scan.is_empty(), "{:?}", scan.functions);
}
#[test]
fn an_edge_fn_with_a_cfg_attr_injected_true_cfg_is_included() {
let scan = scan_one_with_features(
r#"
#[cfg_attr(feature = "outer", cfg(feature = "inner"))]
#[edge]
pub fn hello() {}
"#,
&["outer", "inner"],
);
assert_eq!(scan.names(), vec!["hello"]);
}
#[test]
fn an_edge_fn_with_an_inactive_cfg_attr_injected_cfg_is_included() {
let scan = scan_one_with_features(
r#"
#[cfg_attr(feature = "outer", cfg(feature = "inner"))]
#[edge]
pub fn hello() {}
"#,
&[],
);
assert_eq!(scan.names(), vec!["hello"]);
}
#[test]
fn an_edge_marker_behind_a_nested_cfg_attr_is_found() {
let scan = scan_one_with_features(
r#"
#[cfg_attr(feature = "a", cfg_attr(feature = "b", edge))]
pub fn hello() {}
"#,
&["a", "b"],
);
assert_eq!(scan.names(), vec!["hello"]);
}
#[test]
fn an_edge_marker_behind_a_nested_cfg_attr_with_only_the_outer_condition_on_is_not_found() {
let scan = scan_one_with_features(
r#"
#[cfg_attr(feature = "a", cfg_attr(feature = "b", edge))]
pub fn hello() {}
"#,
&["a"],
);
assert!(scan.is_empty(), "{:?}", scan.functions);
}
#[test]
fn an_edge_fn_with_a_nested_cfg_attr_injected_false_cfg_is_excluded() {
let scan = scan_one_with_features(
r#"
#[cfg_attr(feature = "a", cfg_attr(feature = "b", cfg(feature = "c")))]
#[edge]
pub fn hello() {}
"#,
&["a", "b"],
);
assert!(scan.is_empty(), "{:?}", scan.functions);
}
#[test]
fn guard_attributes_on_the_same_fn_are_recorded() {
let scan = scan_one(
r#"
#[get("/dash")]
#[edge]
#[secured]
fn dash() {}
#[get("/t")]
#[edge]
#[autumn_web::throttle(per_minute = 5)]
fn throttled() {}
#[get("/ok")]
#[edge]
fn ok() {}
"#,
);
let guarded: Vec<&str> = scan.guarded().iter().map(|f| f.name.as_str()).collect();
assert_eq!(guarded, vec!["dash", "throttled"]);
assert_eq!(scan.functions[0].guards, vec!["secured".to_owned()]);
assert_eq!(scan.functions[1].guards, vec!["throttle".to_owned()]);
}
#[test]
fn every_guard_attribute_is_detected() {
for guard in EDGE_GUARD_ATTRS {
let scan = scan_one(&format!("#[edge]\n#[{guard}]\nfn g() {{}}"));
assert_eq!(
scan.guarded().len(),
1,
"#[{guard}] must be detected next to #[edge]"
);
}
}
#[test]
fn registrations_collect_handler_idents() {
let scan = scan_one(
r"
#[edge]
fn greet() {}
#[edge]
fn note() {}
pub fn edge_routes() -> Vec<EdgeRoute> {
autumn_edge::edge_routes![handlers::greet, note,]
}
",
);
assert_eq!(scan.registrations, 1);
assert!(
scan.registered
.contains(&(String::new(), "handlers::greet".to_owned()))
);
assert!(
scan.registered
.contains(&(String::new(), "note".to_owned()))
);
let unregistered: Vec<&str> = scan
.unregistered()
.iter()
.map(|f| f.name.as_str())
.collect();
assert_eq!(unregistered, vec!["greet"]);
assert_eq!(
scan.registered_fns()
.iter()
.map(|f| f.name.as_str())
.collect::<Vec<_>>(),
vec!["note"]
);
}
#[test]
fn a_function_named_edge_routes_is_not_a_registration() {
let scan = scan_one(
r"
#[edge]
fn greet() {}
pub fn edge_routes() -> Vec<EdgeRoute> { Vec::new() }
",
);
assert_eq!(scan.registrations, 0);
assert_eq!(
scan.unregistered()
.iter()
.map(|f| f.name.as_str())
.collect::<Vec<_>>(),
vec!["greet"]
);
}
#[test]
fn unregistered_lists_only_the_missing_handlers() {
let scan = scan_one(
r"
#[edge]
fn greet() {}
#[edge]
fn stats() {}
fn wire() { edge_routes![greet]; }
",
);
let missing: Vec<String> = scan.unregistered().iter().map(|f| f.location()).collect();
assert_eq!(missing.len(), 1);
assert!(missing[0].starts_with("stats @ src/lib.rs:"), "{missing:?}");
}
#[test]
fn inline_modules_are_scanned() {
let scan = scan_one(
r"
mod routes {
#[edge]
pub fn nested() {}
}
",
);
assert_eq!(scan.names(), vec!["nested"]);
}
#[test]
fn line_numbers_point_at_the_declaration() {
let scan = scan_one("// header\n\n#[edge]\nfn marked() {}\n");
assert_eq!(scan.functions[0].line, 4);
}
#[test]
fn unparseable_source_is_skipped() {
let scan = scan_one("fn broken( {");
assert!(scan.is_empty());
assert_eq!(scan.registrations, 0);
}
#[test]
fn registrations_are_found_across_files() {
let scan = scan_sources(&[
("src/handlers.rs", "#[edge]\nfn greet() {}"),
(
"src/lib.rs",
"fn main() { autumn_edge::serve(edge_routes![greet]); }",
),
]);
assert_eq!(scan.registrations, 1);
assert!(scan.unregistered().is_empty());
}
#[test]
fn resolve_reads_src_tree_relative_to_project_root() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("src/routes")).unwrap();
std::fs::create_dir_all(dir.path().join("target")).unwrap();
std::fs::write(
dir.path().join("src/routes/home.rs"),
"#[edge]\npub fn home() {}\n",
)
.unwrap();
std::fs::write(dir.path().join("src/main.rs"), "fn main() {}\n").unwrap();
std::fs::write(dir.path().join("target/gen.rs"), "#[edge]\nfn gen() {}\n").unwrap();
let scan = resolve_edge_scan(dir.path());
assert_eq!(scan.names(), vec!["home"]);
assert_eq!(scan.functions[0].file, "src/routes/home.rs");
assert_eq!(scan.files_scanned, 2);
}
#[test]
fn resolve_without_src_is_empty() {
let dir = tempfile::tempdir().unwrap();
let scan = resolve_edge_scan(dir.path());
assert!(scan.is_empty());
assert_eq!(scan.files_scanned, 0);
}
#[test]
fn cfg_feature_gated_fn_excluded_when_feature_not_default() {
let scan = scan_one_with_features(
r#"
#[cfg(feature = "unused")]
#[edge]
fn hidden() {}
"#,
&[],
);
assert!(scan.is_empty(), "{:?}", scan.functions);
}
#[test]
fn cfg_feature_gated_inline_module_excludes_its_handlers() {
let scan = scan_one_with_features(
r#"
#[cfg(feature = "premium")]
mod routes {
#[edge]
pub fn show() {}
}
"#,
&[],
);
assert!(scan.is_empty(), "{:?}", scan.functions);
}
#[test]
fn cfg_feature_gated_inline_module_included_when_feature_is_default() {
let scan = scan_one_with_features(
r#"
#[cfg(feature = "premium")]
mod routes {
#[edge]
pub fn show() {}
}
"#,
&["premium"],
);
assert_eq!(scan.names(), vec!["show"]);
}
#[test]
fn cfg_feature_gated_inline_module_registration_is_excluded_when_feature_is_off() {
let scan = scan_one_with_features(
r#"
#[edge]
pub fn show() {}
#[cfg(feature = "premium")]
mod wiring {
fn wire() { edge_routes![crate::show]; }
}
"#,
&[],
);
assert!(scan.unregistered().len() == 1, "{:?}", scan.unregistered());
assert!(
scan.registered_fns().is_empty(),
"{:?}",
scan.registered_fns()
);
}
#[test]
fn cfg_feature_gated_inline_module_registration_is_included_when_feature_is_default() {
let scan = scan_one_with_features(
r#"
#[edge]
pub fn show() {}
#[cfg(feature = "premium")]
mod wiring {
fn wire() { edge_routes![crate::show]; }
}
"#,
&["premium"],
);
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
assert_eq!(scan.registered_fns().len(), 1);
}
#[test]
fn cfg_attr_injected_false_cfg_on_an_inline_module_excludes_it_entirely() {
let scan = scan_one_with_features(
r#"
#[edge]
pub fn show() {}
#[cfg_attr(feature = "outer", cfg(feature = "inner"))]
mod premium {
#[edge]
pub fn extra() {}
fn wire() { edge_routes![crate::show]; }
}
"#,
&["outer"],
);
assert_eq!(
scan.names(),
vec!["show".to_owned()],
"{:?}",
scan.functions
);
assert_eq!(scan.unregistered().len(), 1, "{:?}", scan.unregistered());
assert_eq!(scan.unregistered()[0].name, "show");
}
#[test]
fn cfg_attr_injected_true_cfg_on_an_inline_module_includes_it() {
let scan = scan_one_with_features(
r#"
#[edge]
pub fn show() {}
#[cfg_attr(feature = "outer", cfg(feature = "inner"))]
mod premium {
#[edge]
pub fn extra() {}
fn wire() {
edge_routes![crate::show];
edge_routes![crate::premium::extra];
}
}
"#,
&["outer", "inner"],
);
assert_eq!(scan.functions.len(), 2, "{:?}", scan.functions);
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
}
#[test]
fn cfg_feature_gated_pub_inline_module_registration_is_excluded_when_feature_is_off() {
let scan = scan_one_with_features(
r#"
#[edge]
pub fn show() {}
#[cfg(feature = "premium")]
pub mod wiring {
pub fn wire() { edge_routes![crate::show]; }
}
"#,
&[],
);
assert!(scan.unregistered().len() == 1, "{:?}", scan.unregistered());
assert!(
scan.registered_fns().is_empty(),
"{:?}",
scan.registered_fns()
);
}
#[test]
fn cfg_feature_gated_pub_inline_module_registration_is_included_when_feature_is_default() {
let scan = scan_one_with_features(
r#"
#[edge]
pub fn show() {}
#[cfg(feature = "premium")]
pub mod wiring {
pub fn wire() { edge_routes![crate::show]; }
}
"#,
&["premium"],
);
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
assert_eq!(scan.registered_fns().len(), 1);
}
#[test]
fn cfg_feature_gated_impl_block_registration_is_excluded_when_feature_is_off() {
let scan = scan_one_with_features(
r#"
#[edge]
pub fn show() {}
struct Routes;
#[cfg(feature = "premium")]
impl Routes {
fn wire() { edge_routes![crate::show]; }
}
"#,
&[],
);
assert!(scan.unregistered().len() == 1, "{:?}", scan.unregistered());
assert!(
scan.registered_fns().is_empty(),
"{:?}",
scan.registered_fns()
);
}
#[test]
fn cfg_feature_gated_impl_block_registration_is_included_when_feature_is_default() {
let scan = scan_one_with_features(
r#"
#[edge]
pub fn show() {}
struct Routes;
#[cfg(feature = "premium")]
impl Routes {
fn wire() { edge_routes![crate::show]; }
}
"#,
&["premium"],
);
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
assert_eq!(scan.registered_fns().len(), 1);
}
#[test]
fn cfg_feature_gated_impl_block_with_an_arrow_bound_registration_is_excluded_when_feature_is_off()
{
let scan = scan_one_with_features(
r#"
#[edge]
pub fn show() {}
struct Routes<T>(T);
#[cfg(feature = "premium")]
impl<T: Fn() -> u8> Routes<T> {
fn wire() { edge_routes![crate::show]; }
}
"#,
&[],
);
assert_eq!(scan.unregistered().len(), 1, "{:?}", scan.unregistered());
assert!(
scan.registered_fns().is_empty(),
"{:?}",
scan.registered_fns()
);
}
#[test]
fn cfg_feature_gated_fn_registration_is_excluded_when_feature_is_off() {
let scan = scan_one_with_features(
r#"
#[edge]
pub fn show() {}
#[cfg(feature = "premium")]
fn wire() { edge_routes![crate::show]; }
"#,
&[],
);
assert_eq!(scan.unregistered().len(), 1, "{:?}", scan.unregistered());
assert!(
scan.registered_fns().is_empty(),
"{:?}",
scan.registered_fns()
);
}
#[test]
fn cfg_feature_gated_registration_statement_is_excluded_when_feature_is_off() {
let scan = scan_one_with_features(
r#"
#[edge]
pub fn show() {}
fn wire() {
#[cfg(feature = "premium")]
edge_routes![crate::show];
}
"#,
&[],
);
assert_eq!(scan.unregistered().len(), 1, "{:?}", scan.unregistered());
assert!(
scan.registered_fns().is_empty(),
"{:?}",
scan.registered_fns()
);
}
#[test]
fn cfg_feature_gated_statement_with_a_nested_registration_call_is_excluded_when_feature_is_off()
{
let scan = scan_one_with_features(
r#"
#[edge]
pub fn show() {}
fn wire(routes: &mut Vec<()>) {
#[cfg(feature = "premium")]
routes.extend(edge_routes![crate::show]);
}
"#,
&[],
);
assert_eq!(scan.unregistered().len(), 1, "{:?}", scan.unregistered());
assert!(
scan.registered_fns().is_empty(),
"{:?}",
scan.registered_fns()
);
}
#[test]
fn cfg_feature_gated_statement_with_a_nested_registration_call_is_included_when_feature_is_default()
{
let scan = scan_one_with_features(
r#"
#[edge]
pub fn show() {}
fn wire(routes: &mut Vec<()>) {
#[cfg(feature = "premium")]
routes.extend(edge_routes![crate::show]);
}
"#,
&["premium"],
);
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
assert_eq!(scan.registered_fns().len(), 1);
}
#[test]
fn cfg_feature_gated_if_statement_does_not_swallow_the_following_registration() {
let scan = scan_one_with_features(
r#"
#[edge]
pub fn show() {}
#[edge]
pub fn premium() {}
fn wire() {
#[cfg(feature = "premium")]
if true {
edge_routes![crate::premium];
}
edge_routes![crate::show];
}
"#,
&[],
);
assert_eq!(scan.unregistered().len(), 1, "{:?}", scan.unregistered());
assert_eq!(scan.unregistered()[0].name, "premium");
assert_eq!(
scan.registered_fns().len(),
1,
"{:?}",
scan.registered_fns()
);
assert_eq!(scan.registered_fns()[0].name, "show");
}
#[test]
fn cfg_feature_gated_if_statement_registration_is_included_when_feature_is_default() {
let scan = scan_one_with_features(
r#"
#[edge]
pub fn show() {}
#[edge]
pub fn premium() {}
fn wire() {
#[cfg(feature = "premium")]
if true {
edge_routes![crate::premium];
}
edge_routes![crate::show];
}
"#,
&["premium"],
);
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
assert_eq!(
scan.registered_fns().len(),
2,
"{:?}",
scan.registered_fns()
);
}
#[test]
fn cfg_false_const_block_does_not_swallow_the_following_registration() {
let scan = scan_one_with_features(
r#"
#[edge]
pub fn show() {}
#[edge]
pub fn premium() {}
fn wire() {
#[cfg(feature = "premium")]
const {
edge_routes![crate::premium];
}
edge_routes![crate::show];
}
"#,
&[],
);
assert_eq!(scan.unregistered().len(), 1, "{:?}", scan.unregistered());
assert_eq!(scan.unregistered()[0].name, "premium");
assert_eq!(
scan.registered_fns().len(),
1,
"{:?}",
scan.registered_fns()
);
assert_eq!(scan.registered_fns()[0].name, "show");
}
#[test]
fn cfg_false_labeled_loop_does_not_swallow_the_following_registration() {
let scan = scan_one_with_features(
r#"
#[edge]
pub fn show() {}
#[edge]
pub fn premium() {}
fn wire() {
#[cfg(feature = "premium")]
'outer: loop {
edge_routes![crate::premium];
break 'outer;
}
edge_routes![crate::show];
}
"#,
&[],
);
assert_eq!(scan.unregistered().len(), 1, "{:?}", scan.unregistered());
assert_eq!(scan.unregistered()[0].name, "premium");
assert_eq!(
scan.registered_fns().len(),
1,
"{:?}",
scan.registered_fns()
);
assert_eq!(scan.registered_fns()[0].name, "show");
}
#[test]
fn cfg_false_const_item_with_a_braced_initializer_does_not_swallow_the_following_registration()
{
let scan = scan_one_with_features(
r#"
#[edge]
pub fn show() {}
#[edge]
pub fn premium() {}
fn wire() {
#[cfg(feature = "premium")]
const VALUE: () = { edge_routes![crate::premium]; };
edge_routes![crate::show];
}
"#,
&[],
);
assert_eq!(scan.unregistered().len(), 1, "{:?}", scan.unregistered());
assert_eq!(scan.unregistered()[0].name, "premium");
assert_eq!(
scan.registered_fns().len(),
1,
"{:?}",
scan.registered_fns()
);
assert_eq!(scan.registered_fns()[0].name, "show");
}
#[test]
fn cfg_false_struct_item_does_not_swallow_the_following_registration() {
let scan = scan_one_with_features(
r#"
#[edge]
pub fn show() {}
fn wire() {
#[cfg(feature = "premium")]
struct Premium {
field: i32,
}
edge_routes![crate::show];
}
"#,
&[],
);
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
assert_eq!(
scan.registered_fns().len(),
1,
"{:?}",
scan.registered_fns()
);
assert_eq!(scan.registered_fns()[0].name, "show");
}
#[test]
fn cfg_false_pub_enum_and_trait_items_do_not_swallow_the_following_registration() {
let scan = scan_one_with_features(
r#"
#[edge]
pub fn show() {}
fn wire() {
#[cfg(feature = "premium")]
pub enum Premium {
A,
B,
}
#[cfg(feature = "premium")]
trait PremiumTrait {
fn method(&self);
}
edge_routes![crate::show];
}
"#,
&[],
);
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
assert_eq!(
scan.registered_fns().len(),
1,
"{:?}",
scan.registered_fns()
);
assert_eq!(scan.registered_fns()[0].name, "show");
}
#[test]
fn cfg_false_tuple_and_unit_struct_items_do_not_swallow_the_following_registration() {
let scan = scan_one_with_features(
r#"
#[edge]
pub fn show() {}
fn wire() {
#[cfg(feature = "premium")]
struct Premium(i32);
#[cfg(feature = "premium")]
struct PremiumUnit;
edge_routes![crate::show];
}
"#,
&[],
);
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
assert_eq!(
scan.registered_fns().len(),
1,
"{:?}",
scan.registered_fns()
);
assert_eq!(scan.registered_fns()[0].name, "show");
}
#[test]
fn cfg_false_macro_rules_brace_form_does_not_swallow_the_following_registration() {
let scan = scan_one_with_features(
r#"
#[edge]
pub fn show() {}
fn wire() {
#[cfg(feature = "premium")]
macro_rules! unused {
() => {};
}
edge_routes![crate::show];
}
"#,
&[],
);
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
assert_eq!(
scan.registered_fns().len(),
1,
"{:?}",
scan.registered_fns()
);
assert_eq!(scan.registered_fns()[0].name, "show");
}
#[test]
fn cfg_false_macro_rules_paren_form_does_not_swallow_the_following_registration() {
let scan = scan_one_with_features(
r#"
#[edge]
pub fn show() {}
fn wire() {
#[cfg(feature = "premium")]
macro_rules! unused(
() => {};
);
edge_routes![crate::show];
}
"#,
&[],
);
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
assert_eq!(
scan.registered_fns().len(),
1,
"{:?}",
scan.registered_fns()
);
assert_eq!(scan.registered_fns()[0].name, "show");
}
#[test]
fn cfg_false_brace_delimited_macro_invocation_does_not_swallow_the_following_registration() {
let scan = scan_one_with_features(
r#"
#[edge]
pub fn show() {}
macro_rules! configure {
($($t:tt)*) => {};
}
fn wire() {
#[cfg(feature = "premium")]
configure! { a b c }
edge_routes![crate::show];
}
"#,
&[],
);
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
assert_eq!(
scan.registered_fns().len(),
1,
"{:?}",
scan.registered_fns()
);
assert_eq!(scan.registered_fns()[0].name, "show");
}
#[test]
fn cfg_feature_gated_fn_with_a_grouped_return_type_registration_is_excluded_when_feature_is_off()
{
let scan = scan_one_with_features(
r#"
#[edge]
pub fn show() {}
#[cfg(feature = "premium")]
fn wire() -> Result<(), ()> { edge_routes![crate::show]; Ok(()) }
"#,
&[],
);
assert_eq!(scan.unregistered().len(), 1, "{:?}", scan.unregistered());
assert!(
scan.registered_fns().is_empty(),
"{:?}",
scan.registered_fns()
);
}
#[test]
fn cfg_feature_gated_fn_with_a_braced_const_generic_return_type_registration_is_excluded_when_feature_is_off()
{
let scan = scan_one_with_features(
r#"
#[edge]
pub fn show() {}
struct R<const N: usize>;
#[cfg(feature = "premium")]
fn wire<const N: usize>() -> R<{ 1 + 2 }> { edge_routes![crate::show]; R }
"#,
&[],
);
assert_eq!(scan.unregistered().len(), 1, "{:?}", scan.unregistered());
assert!(
scan.registered_fns().is_empty(),
"{:?}",
scan.registered_fns()
);
}
#[test]
fn cfg_feature_gated_fn_with_a_nested_arrow_return_type_registration_is_excluded_when_feature_is_off()
{
let scan = scan_one_with_features(
r#"
#[edge]
pub fn show() {}
#[cfg(feature = "premium")]
fn wire() -> impl Fn() -> () {
edge_routes![crate::show];
|| ()
}
"#,
&[],
);
assert_eq!(scan.unregistered().len(), 1, "{:?}", scan.unregistered());
assert!(
scan.registered_fns().is_empty(),
"{:?}",
scan.registered_fns()
);
}
#[test]
fn cfg_feature_gated_pub_async_fn_registration_is_excluded_when_feature_is_off() {
let scan = scan_one_with_features(
r#"
#[edge]
pub fn show() {}
#[cfg(feature = "premium")]
pub async fn wire() { edge_routes![crate::show]; }
"#,
&[],
);
assert_eq!(scan.unregistered().len(), 1, "{:?}", scan.unregistered());
assert!(
scan.registered_fns().is_empty(),
"{:?}",
scan.registered_fns()
);
}
#[test]
fn cfg_feature_gated_fn_registration_is_included_when_feature_is_default() {
let scan = scan_one_with_features(
r#"
#[edge]
pub fn show() {}
#[cfg(feature = "premium")]
fn wire() { edge_routes![crate::show]; }
"#,
&["premium"],
);
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
assert_eq!(scan.registered_fns().len(), 1);
}
#[test]
fn a_body_less_fn_signature_does_not_swallow_a_later_items_body() {
let scan = scan_one(
r"
#[edge]
pub fn show() {}
trait Greeter {
fn bar();
}
mod wiring {
fn wire() { edge_routes![crate::show]; }
}
",
);
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
assert_eq!(scan.registered_fns().len(), 1);
}
#[test]
fn cfg_feature_gated_fn_with_a_return_type_registration_is_excluded_when_feature_is_off() {
let scan = scan_one_with_features(
r#"
#[edge]
pub fn show() {}
#[cfg(feature = "premium")]
fn wire() -> Vec<String> { edge_routes![crate::show]; Vec::new() }
"#,
&[],
);
assert_eq!(scan.unregistered().len(), 1, "{:?}", scan.unregistered());
assert!(
scan.registered_fns().is_empty(),
"{:?}",
scan.registered_fns()
);
}
#[test]
fn cfg_feature_gated_generic_fn_registration_is_excluded_when_feature_is_off() {
let scan = scan_one_with_features(
r#"
#[edge]
pub fn show() {}
#[cfg(feature = "premium")]
fn wire<T: Default>() { edge_routes![crate::show]; }
"#,
&[],
);
assert_eq!(scan.unregistered().len(), 1, "{:?}", scan.unregistered());
assert!(
scan.registered_fns().is_empty(),
"{:?}",
scan.registered_fns()
);
}
#[test]
fn cfg_feature_gated_generic_fn_with_a_where_clause_registration_is_excluded_when_feature_is_off()
{
let scan = scan_one_with_features(
r#"
#[edge]
pub fn show() {}
#[cfg(feature = "premium")]
fn wire<T>() where T: Default { edge_routes![crate::show]; }
"#,
&[],
);
assert_eq!(scan.unregistered().len(), 1, "{:?}", scan.unregistered());
assert!(
scan.registered_fns().is_empty(),
"{:?}",
scan.registered_fns()
);
}
#[test]
fn cfg_feature_gated_generic_fn_with_an_arrow_bound_registration_is_excluded_when_feature_is_off()
{
let scan = scan_one_with_features(
r#"
#[edge]
pub fn show() {}
#[cfg(feature = "premium")]
fn wire<T: Fn() -> u8>() { edge_routes![crate::show]; }
"#,
&[],
);
assert_eq!(scan.unregistered().len(), 1, "{:?}", scan.unregistered());
assert!(
scan.registered_fns().is_empty(),
"{:?}",
scan.registered_fns()
);
}
#[test]
fn cfg_feature_gated_fn_included_when_feature_is_default() {
let scan = scan_one_with_features(
r#"
#[cfg(feature = "default-on")]
#[edge]
fn shown() {}
"#,
&["default-on"],
);
assert_eq!(scan.names(), vec!["shown"]);
}
#[test]
fn cfg_not_feature_included_when_feature_absent() {
let scan = scan_one_with_features(
r#"
#[cfg(not(feature = "unused"))]
#[edge]
fn shown() {}
"#,
&[],
);
assert_eq!(scan.names(), vec!["shown"]);
}
#[test]
fn cfg_false_excludes_and_cfg_true_includes() {
let scan = scan_one(
r"
#[cfg(false)]
#[edge]
fn hidden() {}
#[cfg(true)]
#[edge]
fn shown() {}
",
);
assert_eq!(scan.names(), vec!["shown"]);
}
#[test]
fn cfg_false_excludes_a_registration() {
let scan = scan_one(
r"
#[edge]
pub fn show() {}
#[cfg(false)]
fn wire() { edge_routes![show]; }
",
);
assert!(
scan.registered_fns().is_empty(),
"{:?}",
scan.registered_fns()
);
assert_eq!(scan.unregistered().len(), 1, "{:?}", scan.unregistered());
}
#[test]
fn cfg_any_included_when_one_default_feature_present() {
let scan = scan_one_with_features(
r#"
#[cfg(any(feature = "a", feature = "unused"))]
#[edge]
fn shown() {}
"#,
&["a"],
);
assert_eq!(scan.names(), vec!["shown"]);
}
#[test]
fn cfg_all_excludes_when_one_branch_is_not_default() {
let scan = scan_one_with_features(
r#"
#[cfg(all(feature = "a", feature = "unused"))]
#[edge]
fn hidden() {}
"#,
&["a"],
);
assert!(scan.is_empty(), "{:?}", scan.functions);
}
#[test]
fn cfg_non_feature_predicate_stays_conservative() {
let scan = scan_one_with_features(
r#"
#[cfg(target_os = "linux")]
#[edge]
fn maybe() {}
"#,
&[],
);
assert_eq!(
scan.names(),
vec!["maybe"],
"a non-feature cfg predicate must stay conservative (included)"
);
}
#[test]
fn cfg_multiple_attributes_are_anded() {
let scan = scan_one_with_features(
r#"
#[cfg(feature = "a")]
#[cfg(feature = "unused")]
#[edge]
fn hidden() {}
"#,
&["a"],
);
assert!(scan.is_empty(), "{:?}", scan.functions);
}
#[test]
fn compute_default_features_expands_transitive_defaults() {
let manifest = r#"
[features]
default = ["a"]
a = ["b"]
b = []
"#;
let enabled = enabled_features_from_manifest(manifest, &[]);
assert!(enabled.contains("a"));
assert!(enabled.contains("b"));
}
#[test]
fn enabled_features_from_manifest_includes_a_requested_feature() {
let manifest = r"
[features]
default = []
other = []
";
let enabled = enabled_features_from_manifest(manifest, &["premium"]);
assert!(enabled.contains("premium"));
assert!(!enabled.contains("other"));
}
#[test]
fn a_package_qualified_requested_feature_resolves_to_its_bare_name() {
let manifest = r#"
[package]
name = "blog"
[features]
default = []
"#;
let enabled = enabled_features_from_manifest(manifest, &["blog/extra-routes"]);
assert!(enabled.contains("extra-routes"));
assert!(!enabled.iter().any(|f| f.contains('/')));
}
#[test]
fn a_dependency_qualified_requested_feature_is_not_mistaken_for_a_local_one() {
let manifest = r#"
[package]
name = "blog"
[features]
default = []
extra = []
"#;
let enabled = enabled_features_from_manifest(manifest, &["dep/extra"]);
assert!(!enabled.contains("extra"));
}
#[test]
fn the_default_feature_itself_is_enabled_when_declared() {
let manifest = r#"
[features]
default = ["a"]
a = []
"#;
let enabled = enabled_features_from_manifest(manifest, &[]);
assert!(enabled.contains("default"));
assert!(enabled.contains("a"));
}
#[test]
fn the_default_feature_is_not_enabled_with_no_features_table() {
let manifest = r#"
[package]
name = "blog"
"#;
let enabled = enabled_features_from_manifest(manifest, &[]);
assert!(!enabled.contains("default"));
}
#[test]
fn the_default_feature_is_not_enabled_when_the_features_table_omits_it() {
let manifest = r"
[features]
other = []
";
let enabled = enabled_features_from_manifest(manifest, &[]);
assert!(!enabled.contains("default"));
}
#[test]
fn the_default_feature_is_enabled_when_declared_empty() {
let manifest = r"
[features]
default = []
other = []
";
let enabled = enabled_features_from_manifest(manifest, &[]);
assert!(enabled.contains("default"));
assert!(!enabled.contains("other"));
}
#[test]
fn a_strong_dependency_feature_reference_also_enables_the_dependency_itself() {
let manifest = r#"
[dependencies]
dep = { version = "1", optional = true }
[features]
default = ["dep/extra"]
"#;
let enabled = enabled_features_from_manifest(manifest, &[]);
assert!(enabled.contains("dep"));
assert!(!enabled.contains("extra"));
assert!(!enabled.iter().any(|f| f.contains('/')));
}
#[test]
fn a_strong_reference_to_a_normal_dependency_does_not_enable_a_same_named_feature() {
let manifest = r#"
[dependencies]
foo = { version = "1" }
[features]
default = ["foo/extra"]
foo = []
"#;
let enabled = enabled_features_from_manifest(manifest, &[]);
assert!(!enabled.contains("foo"));
}
#[test]
fn a_dep_colon_reference_suppresses_the_implicit_feature_even_via_an_unrelated_entry() {
let manifest = r#"
[dependencies]
foo = { version = "1", optional = true }
[features]
default = ["foo/bar"]
explicit = ["dep:foo"]
"#;
let enabled = enabled_features_from_manifest(manifest, &[]);
assert!(!enabled.contains("foo"));
}
#[test]
fn an_explicit_feature_sharing_an_optional_dependencys_name_is_still_enabled() {
let manifest = r#"
[dependencies]
foo = { version = "1", optional = true }
[features]
default = ["foo/bar"]
foo = []
explicit = ["dep:foo"]
"#;
let enabled = enabled_features_from_manifest(manifest, &[]);
assert!(enabled.contains("foo"), "{enabled:?}");
}
#[test]
fn a_dep_colon_reference_to_an_unrelated_package_does_not_suppress_anything() {
let manifest = r#"
[dependencies]
foo = { version = "1", optional = true }
other = { version = "1", optional = true }
[features]
default = ["foo/bar"]
explicit = ["dep:other"]
"#;
let enabled = enabled_features_from_manifest(manifest, &[]);
assert!(enabled.contains("foo"));
}
#[test]
fn a_target_specific_optional_dependency_feature_reference_also_enables_the_dependency() {
let manifest = r#"
[target.'cfg(target_arch = "wasm32")'.dependencies]
dep = { version = "1", optional = true }
[features]
default = ["dep/extra"]
"#;
let enabled = enabled_features_from_manifest(manifest, &[]);
assert!(enabled.contains("dep"));
}
#[test]
fn a_literal_wasm32_wasip1_target_table_also_enables_the_dependency() {
let manifest = r#"
[target.wasm32-wasip1.dependencies]
dep = { version = "1", optional = true }
[features]
default = ["dep/extra"]
"#;
let enabled = enabled_features_from_manifest(manifest, &[]);
assert!(enabled.contains("dep"));
}
#[test]
fn a_target_specific_dependency_whose_target_cannot_be_the_edge_capsule_is_not_enabled() {
let manifest = r#"
[target.'cfg(windows)'.dependencies]
dep = { version = "1", optional = true }
[features]
default = ["dep/extra"]
"#;
let enabled = enabled_features_from_manifest(manifest, &[]);
assert!(!enabled.contains("dep"));
}
#[test]
fn a_not_wasm32_target_specific_dependency_is_not_enabled() {
let manifest = r#"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
dep = { version = "1", optional = true }
[features]
default = ["dep/extra"]
"#;
let enabled = enabled_features_from_manifest(manifest, &[]);
assert!(!enabled.contains("dep"));
}
#[test]
fn a_not_windows_target_specific_dependency_also_enables_the_dependency() {
let manifest = r#"
[target.'cfg(not(windows))'.dependencies]
dep = { version = "1", optional = true }
[features]
default = ["dep/extra"]
"#;
let enabled = enabled_features_from_manifest(manifest, &[]);
assert!(enabled.contains("dep"));
}
#[test]
fn a_windows_target_specific_dependency_is_not_enabled() {
let manifest = r#"
[target.'cfg(windows)'.dependencies]
dep = { version = "1", optional = true }
[features]
default = ["dep/extra"]
"#;
let enabled = enabled_features_from_manifest(manifest, &[]);
assert!(!enabled.contains("dep"));
}
#[test]
fn a_literal_true_target_specific_dependency_also_enables_the_dependency() {
let manifest = r#"
[target.'cfg(true)'.dependencies]
dep = { version = "1", optional = true }
[features]
default = ["dep/extra"]
"#;
let enabled = enabled_features_from_manifest(manifest, &[]);
assert!(enabled.contains("dep"));
}
#[test]
fn a_literal_false_target_specific_dependency_is_not_enabled() {
let manifest = r#"
[target.'cfg(false)'.dependencies]
dep = { version = "1", optional = true }
[features]
default = ["dep/extra"]
"#;
let enabled = enabled_features_from_manifest(manifest, &[]);
assert!(!enabled.contains("dep"));
}
#[test]
fn a_windows_target_specific_dependency_is_enabled_under_resolver_v1() {
let manifest = r#"
[target.'cfg(windows)'.dependencies]
dep = { version = "1", optional = true }
[features]
default = ["dep/extra"]
"#;
let enabled = enabled_features_from_manifest_for_resolver(manifest, &[], true);
assert!(enabled.contains("dep"));
}
#[test]
fn a_debug_assertions_target_specific_dependency_is_enabled() {
let manifest = r#"
[target.'cfg(debug_assertions)'.dependencies]
dep = { version = "1", optional = true }
[features]
default = ["dep/extra"]
"#;
let enabled = enabled_features_from_manifest(manifest, &[]);
assert!(enabled.contains("dep"), "{enabled:?}");
}
#[test]
fn a_not_unix_target_specific_dependency_also_enables_the_dependency() {
let manifest = r#"
[target.'cfg(not(unix))'.dependencies]
dep = { version = "1", optional = true }
[features]
default = ["dep/extra"]
"#;
let enabled = enabled_features_from_manifest(manifest, &[]);
assert!(enabled.contains("dep"));
}
#[test]
fn a_not_unknown_bare_flag_target_specific_dependency_also_enables_the_dependency() {
let manifest = r#"
[target.'cfg(not(my_custom_flag))'.dependencies]
dep = { version = "1", optional = true }
[features]
default = ["dep/extra"]
"#;
let enabled = enabled_features_from_manifest(manifest, &[]);
assert!(enabled.contains("dep"), "{enabled:?}");
}
#[test]
fn an_unknown_bare_flag_target_specific_dependency_is_not_enabled() {
let manifest = r#"
[target.'cfg(my_custom_flag)'.dependencies]
dep = { version = "1", optional = true }
[features]
default = ["dep/extra"]
"#;
let enabled = enabled_features_from_manifest(manifest, &[]);
assert!(!enabled.contains("dep"), "{enabled:?}");
}
#[test]
fn a_not_unknown_valued_target_specific_dependency_also_enables_the_dependency() {
let manifest = r#"
[target.'cfg(not(my_key = "x"))'.dependencies]
dep = { version = "1", optional = true }
[features]
default = ["dep/extra"]
"#;
let enabled = enabled_features_from_manifest(manifest, &[]);
assert!(enabled.contains("dep"), "{enabled:?}");
}
#[test]
fn an_unknown_valued_target_specific_dependency_is_not_enabled() {
let manifest = r#"
[target.'cfg(my_key = "x")'.dependencies]
dep = { version = "1", optional = true }
[features]
default = ["dep/extra"]
"#;
let enabled = enabled_features_from_manifest(manifest, &[]);
assert!(!enabled.contains("dep"), "{enabled:?}");
}
#[test]
fn lexically_normalize_path_collapses_a_parent_dir_component() {
assert_eq!(
lexically_normalize_path(Path::new("/project/src/sub/../app.rs")),
Path::new("/project/src/app.rs")
);
}
#[test]
fn lexically_normalize_path_collapses_multiple_parent_dir_components() {
assert_eq!(
lexically_normalize_path(Path::new("/project/a/b/../../c")),
Path::new("/project/c")
);
}
#[test]
fn lexically_normalize_path_keeps_a_leading_parent_dir_with_nothing_to_pop() {
assert_eq!(
lexically_normalize_path(Path::new("../a/b")),
Path::new("../a/b")
);
}
#[test]
fn lexically_normalize_path_keeps_consecutive_leading_parent_dir_components() {
assert_eq!(
lexically_normalize_path(Path::new("../../shared/lib.rs")),
Path::new("../../shared/lib.rs")
);
}
#[test]
fn lexically_normalize_path_drops_current_dir_components() {
assert_eq!(
lexically_normalize_path(Path::new("/project/./src/./app.rs")),
Path::new("/project/src/app.rs")
);
}
#[test]
fn resolver_v1_is_in_effect_honors_an_explicit_resolver_field() {
let table: toml::Table = toml::from_str(
"[package]\nname = \"demo\"\nversion = \"0.1.0\"\nedition = \"2021\"\nresolver = \"1\"\n",
)
.unwrap();
let dir = tempfile::tempdir().unwrap();
assert!(resolver_v1_is_in_effect(dir.path(), Some(&table)));
}
#[test]
fn resolver_v1_is_in_effect_is_false_for_an_explicit_resolver_2_edition_2018() {
let table: toml::Table = toml::from_str(
"[package]\nname = \"demo\"\nversion = \"0.1.0\"\nedition = \"2018\"\nresolver = \"2\"\n",
)
.unwrap();
let dir = tempfile::tempdir().unwrap();
assert!(!resolver_v1_is_in_effect(dir.path(), Some(&table)));
}
#[test]
fn resolver_v1_is_in_effect_defaults_from_edition_2018() {
let table: toml::Table =
toml::from_str("[package]\nname = \"demo\"\nversion = \"0.1.0\"\nedition = \"2018\"\n")
.unwrap();
let dir = tempfile::tempdir().unwrap();
assert!(resolver_v1_is_in_effect(dir.path(), Some(&table)));
}
#[test]
fn resolver_v1_is_in_effect_defaults_from_edition_2021() {
let table: toml::Table =
toml::from_str("[package]\nname = \"demo\"\nversion = \"0.1.0\"\nedition = \"2021\"\n")
.unwrap();
let dir = tempfile::tempdir().unwrap();
assert!(!resolver_v1_is_in_effect(dir.path(), Some(&table)));
}
#[test]
fn resolver_v1_is_in_effect_defaults_to_true_with_no_edition_at_all() {
let table: toml::Table =
toml::from_str("[package]\nname = \"demo\"\nversion = \"0.1.0\"\n").unwrap();
let dir = tempfile::tempdir().unwrap();
assert!(resolver_v1_is_in_effect(dir.path(), Some(&table)));
}
#[test]
fn resolver_v1_is_in_effect_honors_an_ancestor_workspaces_explicit_resolver() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("Cargo.toml"),
"[workspace]\nmembers = [\"mainpkg\"]\nresolver = \"1\"\n",
)
.unwrap();
std::fs::create_dir_all(dir.path().join("mainpkg")).unwrap();
let member: toml::Table = toml::from_str(
"[package]\nname = \"mainpkg\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
)
.unwrap();
assert!(resolver_v1_is_in_effect(
&dir.path().join("mainpkg"),
Some(&member)
));
}
#[test]
fn resolver_v1_is_in_effect_ignores_an_ancestor_workspace_that_excludes_this_package() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("Cargo.toml"),
"[workspace]\nmembers = [\"other\"]\nexclude = [\"mainpkg\"]\nresolver = \"2\"\n",
)
.unwrap();
std::fs::create_dir_all(dir.path().join("mainpkg")).unwrap();
let member: toml::Table = toml::from_str(
"[package]\nname = \"mainpkg\"\nversion = \"0.1.0\"\nedition = \"2018\"\n",
)
.unwrap();
assert!(resolver_v1_is_in_effect(
&dir.path().join("mainpkg"),
Some(&member)
));
}
#[test]
fn resolver_v1_is_in_effect_still_honors_an_ancestor_workspace_that_excludes_something_else() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("Cargo.toml"),
"[workspace]\nmembers = [\"mainpkg\"]\nexclude = [\"other\"]\nresolver = \"2\"\n",
)
.unwrap();
std::fs::create_dir_all(dir.path().join("mainpkg")).unwrap();
let member: toml::Table = toml::from_str(
"[package]\nname = \"mainpkg\"\nversion = \"0.1.0\"\nedition = \"2018\"\n",
)
.unwrap();
assert!(!resolver_v1_is_in_effect(
&dir.path().join("mainpkg"),
Some(&member)
));
}
#[test]
fn resolver_v1_is_in_effect_ignores_an_ancestor_workspace_excluding_via_a_glob() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("Cargo.toml"),
"[workspace]\nmembers = []\nexclude = [\"crates/*\"]\nresolver = \"2\"\n",
)
.unwrap();
std::fs::create_dir_all(dir.path().join("crates/mainpkg")).unwrap();
let member: toml::Table = toml::from_str(
"[package]\nname = \"mainpkg\"\nversion = \"0.1.0\"\nedition = \"2018\"\n",
)
.unwrap();
assert!(resolver_v1_is_in_effect(
&dir.path().join("crates/mainpkg"),
Some(&member)
));
}
#[test]
fn resolver_v1_is_in_effect_does_not_extend_a_members_glob_across_an_extra_segment() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("Cargo.toml"),
"[workspace]\nmembers = [\"crates/*\"]\nexclude = [\"crates/group/app\"]\nresolver = \"2\"\n",
)
.unwrap();
std::fs::create_dir_all(dir.path().join("crates/group/app")).unwrap();
let member: toml::Table =
toml::from_str("[package]\nname = \"app\"\nversion = \"0.1.0\"\nedition = \"2018\"\n")
.unwrap();
assert!(resolver_v1_is_in_effect(
&dir.path().join("crates/group/app"),
Some(&member)
));
}
#[test]
fn resolver_v1_is_in_effect_lets_explicit_membership_override_an_overlapping_exclude() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("Cargo.toml"),
"[workspace]\nmembers = [\"crates/app\"]\nexclude = [\"crates\"]\nresolver = \"2\"\n",
)
.unwrap();
std::fs::create_dir_all(dir.path().join("crates/app")).unwrap();
let member: toml::Table =
toml::from_str("[package]\nname = \"app\"\nversion = \"0.1.0\"\nedition = \"2018\"\n")
.unwrap();
assert!(!resolver_v1_is_in_effect(
&dir.path().join("crates/app"),
Some(&member)
));
}
#[test]
fn resolver_v1_is_in_effect_defaults_true_for_a_virtual_workspace_with_no_explicit_resolver() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("Cargo.toml"),
"[workspace]\nmembers = [\"mainpkg\"]\n",
)
.unwrap();
std::fs::create_dir_all(dir.path().join("mainpkg")).unwrap();
let member: toml::Table = toml::from_str(
"[package]\nname = \"mainpkg\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
)
.unwrap();
assert!(resolver_v1_is_in_effect(
&dir.path().join("mainpkg"),
Some(&member)
));
}
#[test]
fn resolver_v1_is_in_effect_defaults_from_the_root_packages_edition_in_a_non_virtual_workspace()
{
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"root\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[workspace]\nmembers = [\"mainpkg\"]\n",
)
.unwrap();
std::fs::create_dir_all(dir.path().join("mainpkg")).unwrap();
let member: toml::Table = toml::from_str(
"[package]\nname = \"mainpkg\"\nversion = \"0.1.0\"\nedition = \"2018\"\n",
)
.unwrap();
assert!(!resolver_v1_is_in_effect(
&dir.path().join("mainpkg"),
Some(&member)
));
}
#[test]
fn resolver_v1_is_in_effect_resolves_the_root_packages_inherited_workspace_edition() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"root\"\nversion = \"0.1.0\"\nedition.workspace = true\n\n\
[workspace]\nmembers = [\"mainpkg\"]\n\n\
[workspace.package]\nedition = \"2021\"\n",
)
.unwrap();
std::fs::create_dir_all(dir.path().join("mainpkg")).unwrap();
let member: toml::Table = toml::from_str(
"[package]\nname = \"mainpkg\"\nversion = \"0.1.0\"\nedition = \"2018\"\n",
)
.unwrap();
assert!(!resolver_v1_is_in_effect(
&dir.path().join("mainpkg"),
Some(&member)
));
}
#[test]
fn resolver_v1_is_in_effect_honors_a_self_owned_workspace_resolver() {
let table: toml::Table = toml::from_str(
"[package]\nname = \"root\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[workspace]\nmembers = []\nresolver = \"1\"\n",
)
.unwrap();
let dir = tempfile::tempdir().unwrap();
assert!(resolver_v1_is_in_effect(dir.path(), Some(&table)));
}
#[test]
fn resolve_edge_scan_includes_a_resolver_v1_unified_target_specific_feature_route() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("src")).unwrap();
std::fs::write(
dir.path().join("Cargo.toml"),
r#"
[package]
name = "demo"
version = "0.1.0"
edition = "2018"
[features]
default = ["dep/extra"]
[target.'cfg(windows)'.dependencies]
dep = { version = "1", optional = true }
"#,
)
.unwrap();
std::fs::write(
dir.path().join("src/lib.rs"),
r#"
#[cfg(feature = "dep")]
#[edge]
pub fn show() {}
"#,
)
.unwrap();
let scan = resolve_edge_scan(dir.path());
assert_eq!(scan.names(), vec!["show"]);
}
#[test]
fn resolve_edge_scan_excludes_a_resolver_v2_target_specific_feature_route() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("src")).unwrap();
std::fs::write(
dir.path().join("Cargo.toml"),
r#"
[package]
name = "demo"
version = "0.1.0"
edition = "2021"
[features]
default = ["dep/extra"]
[target.'cfg(windows)'.dependencies]
dep = { version = "1", optional = true }
"#,
)
.unwrap();
std::fs::write(
dir.path().join("src/lib.rs"),
r#"
#[cfg(feature = "dep")]
#[edge]
pub fn show() {}
"#,
)
.unwrap();
let scan = resolve_edge_scan(dir.path());
assert!(scan.is_empty());
}
#[test]
fn a_bare_known_key_inside_not_also_enables_the_dependency() {
let manifest = r#"
[target.'cfg(not(target_has_atomic))'.dependencies]
dep = { version = "1", optional = true }
[features]
default = ["dep/extra"]
"#;
let enabled = enabled_features_from_manifest(manifest, &[]);
assert!(enabled.contains("dep"));
}
#[test]
fn a_bare_known_key_target_specific_dependency_is_not_enabled() {
let manifest = r#"
[target.'cfg(target_has_atomic)'.dependencies]
dep = { version = "1", optional = true }
[features]
default = ["dep/extra"]
"#;
let enabled = enabled_features_from_manifest(manifest, &[]);
assert!(!enabled.contains("dep"));
}
#[test]
fn a_target_os_wasi_target_specific_dependency_also_enables_the_dependency() {
let manifest = r#"
[target.'cfg(target_os = "wasi")'.dependencies]
dep = { version = "1", optional = true }
[features]
default = ["dep/extra"]
"#;
let enabled = enabled_features_from_manifest(manifest, &[]);
assert!(enabled.contains("dep"));
}
#[test]
fn a_target_family_wasm_target_specific_dependency_also_enables_the_dependency() {
let manifest = r#"
[target.'cfg(target_family = "wasm")'.dependencies]
dep = { version = "1", optional = true }
[features]
default = ["dep/extra"]
"#;
let enabled = enabled_features_from_manifest(manifest, &[]);
assert!(enabled.contains("dep"));
}
#[test]
fn further_wasm32_wasip1_target_cfg_keys_also_enable_the_dependency() {
for cfg in [
r#"target_env = "p1""#,
r#"target_pointer_width = "32""#,
r#"target_vendor = "unknown""#,
r#"target_endian = "little""#,
] {
let manifest = format!(
r#"
[target.'cfg({cfg})'.dependencies]
dep = {{ version = "1", optional = true }}
[features]
default = ["dep/extra"]
"#
);
let enabled = enabled_features_from_manifest(&manifest, &[]);
assert!(
enabled.contains("dep"),
"cfg({cfg}) should match: {enabled:?}"
);
}
}
#[test]
fn a_target_os_linux_target_specific_dependency_is_not_enabled() {
let manifest = r#"
[target.'cfg(target_os = "linux")'.dependencies]
dep = { version = "1", optional = true }
[features]
default = ["dep/extra"]
"#;
let enabled = enabled_features_from_manifest(manifest, &[]);
assert!(!enabled.contains("dep"));
}
#[test]
fn multi_valued_wasm32_wasip1_target_cfg_keys_also_enable_the_dependency() {
let manifest = r#"
[target.'cfg(target_has_atomic = "32")'.dependencies]
dep = { version = "1", optional = true }
[features]
default = ["dep/extra"]
"#;
let enabled = enabled_features_from_manifest(manifest, &[]);
assert!(enabled.contains("dep"), "{enabled:?}");
}
#[test]
fn a_target_feature_target_specific_dependency_is_never_enabled() {
let manifest = r#"
[target.'cfg(target_feature = "crt-static")'.dependencies]
dep = { version = "1", optional = true }
[features]
default = ["dep/extra"]
"#;
let enabled = enabled_features_from_manifest(manifest, &[]);
assert!(!enabled.contains("dep"), "{enabled:?}");
}
#[test]
fn a_negated_target_feature_target_specific_dependency_is_always_enabled() {
let manifest = r#"
[target.'cfg(not(target_feature = "crt-static"))'.dependencies]
dep = { version = "1", optional = true }
[features]
default = ["dep/extra"]
"#;
let enabled = enabled_features_from_manifest(manifest, &[]);
assert!(enabled.contains("dep"), "{enabled:?}");
}
#[test]
fn a_target_has_atomic_value_the_target_does_not_report_is_not_enabled() {
let manifest = r#"
[target.'cfg(target_has_atomic = "128")'.dependencies]
dep = { version = "1", optional = true }
[features]
default = ["dep/extra"]
"#;
let enabled = enabled_features_from_manifest(manifest, &[]);
assert!(!enabled.contains("dep"));
}
#[test]
fn a_panic_abort_target_specific_dependency_also_enables_the_dependency() {
let manifest = r#"
[target.'cfg(panic = "abort")'.dependencies]
dep = { version = "1", optional = true }
[features]
default = ["dep/extra"]
"#;
let enabled = enabled_features_from_manifest(manifest, &[]);
assert!(enabled.contains("dep"));
}
#[test]
fn a_weak_dependency_feature_reference_does_not_enable_the_dependency() {
let manifest = r#"
[features]
default = ["dep?/extra"]
"#;
let enabled = enabled_features_from_manifest(manifest, &[]);
assert!(!enabled.contains("dep"));
}
#[test]
fn cfg_transitive_default_feature_is_included() {
let mut features = BTreeSet::new();
features.insert("a".to_owned());
features.insert("b".to_owned());
let scan = scan_sources_with_features(
&[(
"src/main.rs",
r#"
#[cfg(feature = "b")]
#[edge]
fn shown() {}
"#,
)],
&features,
);
assert_eq!(scan.names(), vec!["shown"]);
}
#[test]
fn resolve_edge_scan_honors_cargo_toml_default_features() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("src")).unwrap();
std::fs::write(
dir.path().join("Cargo.toml"),
r#"
[package]
name = "demo"
version = "0.1.0"
[features]
default = ["a"]
a = ["b"]
unused = []
"#,
)
.unwrap();
std::fs::write(
dir.path().join("src/main.rs"),
r#"
#[cfg(feature = "b")]
#[edge]
fn shown() {}
#[cfg(feature = "unused")]
#[edge]
fn hidden() {}
"#,
)
.unwrap();
let scan = resolve_edge_scan(dir.path());
assert_eq!(scan.names(), vec!["shown"]);
}
#[test]
fn resolve_edge_scan_derives_the_rust_crate_name_from_a_hyphenated_package() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("src")).unwrap();
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"my-app\"\nversion = \"0.1.0\"\n",
)
.unwrap();
std::fs::write(
dir.path().join("src/lib.rs"),
"#[edge]\npub fn show() {}\nfn wire() { edge_routes![my_app::show]; }\n",
)
.unwrap();
let scan = resolve_edge_scan(dir.path());
assert_eq!(scan.crate_name.as_deref(), Some("my_app"));
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
}
#[test]
fn rust_crate_name_from_manifest_converts_hyphens_to_underscores() {
let table =
toml::from_str::<toml::Table>("[package]\nname = \"my-app\"\nversion = \"0.1.0\"\n")
.unwrap();
assert_eq!(
rust_crate_name_from_manifest(&table).as_deref(),
Some("my_app")
);
}
#[test]
fn rust_crate_name_from_manifest_prefers_an_explicit_lib_name() {
let table = toml::from_str::<toml::Table>(
"[package]\nname = \"my-app\"\nversion = \"0.1.0\"\n\n[lib]\nname = \"totally_different\"\n",
)
.unwrap();
assert_eq!(
rust_crate_name_from_manifest(&table).as_deref(),
Some("totally_different")
);
}
#[test]
fn resolve_edge_scan_with_extra_file_honors_explicitly_requested_features() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("src")).unwrap();
std::fs::write(
dir.path().join("Cargo.toml"),
r#"
[package]
name = "demo"
version = "0.1.0"
[features]
default = []
premium = []
"#,
)
.unwrap();
std::fs::write(
dir.path().join("src/main.rs"),
r#"
#[cfg(feature = "premium")]
#[edge]
fn premium_only() {}
"#,
)
.unwrap();
assert!(resolve_edge_scan(dir.path()).is_empty());
let scan = resolve_edge_scan_with_extra_file(dir.path(), &["premium"], None);
assert_eq!(scan.names(), vec!["premium_only"]);
}
#[test]
fn qualified_registration_does_not_match_a_same_named_fn_in_another_module() {
let scan = scan_one(
r"
mod users {
#[edge]
pub fn show() {}
}
mod admin {
#[edge]
pub fn show() {}
}
fn wire() { edge_routes![users::show]; }
",
);
let unregistered: Vec<&EdgeFn> = scan.unregistered();
assert_eq!(unregistered.len(), 1, "{unregistered:?}");
assert_eq!(unregistered[0].name, "show");
assert_eq!(unregistered[0].module_path, vec!["admin".to_owned()]);
let registered: Vec<&str> = scan
.registered_fns()
.iter()
.map(|f| f.name.as_str())
.collect();
assert_eq!(registered, vec!["show"]);
assert_eq!(
scan.registered_fns()[0].module_path,
vec!["users".to_owned()]
);
}
#[test]
fn qualified_registration_matches_a_genuinely_nested_fn() {
let scan = scan_one(
r"
mod routes {
#[edge]
pub fn nested() {}
}
fn wire() { edge_routes![routes::nested]; }
",
);
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
assert_eq!(scan.registered_fns().len(), 1);
}
#[test]
fn a_crate_qualified_registration_matches_its_module_path() {
let scan = scan_one(
r"
mod users {
#[edge]
pub fn show() {}
}
fn wire() { edge_routes![crate::users::show]; }
",
);
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
assert_eq!(scan.registered_fns().len(), 1);
}
#[test]
fn a_qualified_registration_does_not_wildcard_match_a_crate_root_fn() {
let scan = scan_one(
r"
#[edge]
pub fn show() {}
fn wire() { edge_routes![users::show]; }
",
);
let unregistered: Vec<&EdgeFn> = scan.unregistered();
assert_eq!(unregistered.len(), 1, "{unregistered:?}");
assert_eq!(unregistered[0].name, "show");
assert!(unregistered[0].module_path.is_empty());
assert!(scan.registered_fns().is_empty());
}
#[test]
fn a_self_qualified_registration_resolves_to_its_enclosing_module() {
let scan = scan_one(
r"
mod users {
#[edge]
pub fn show() {}
fn wire() { edge_routes![self::show]; }
}
",
);
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
assert_eq!(scan.registered_fns().len(), 1);
}
#[test]
fn a_super_qualified_registration_resolves_to_the_parent_module() {
let scan = scan_one(
r"
#[edge]
pub fn show() {}
mod admin {
fn wire() { edge_routes![super::show]; }
}
",
);
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
assert_eq!(scan.registered_fns().len(), 1);
}
#[test]
fn a_crate_name_qualified_registration_matches_its_module_path() {
let mut scan = scan_one(
r"
#[edge]
pub fn show() {}
fn wire() { edge_routes![edgeapp::show]; }
",
);
scan.crate_name = Some("edgeapp".to_owned());
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
assert_eq!(scan.registered_fns().len(), 1);
}
#[test]
fn a_raw_identifier_crate_name_qualified_registration_matches_its_module_path() {
let mut scan = scan_one(
r"
#[edge]
pub fn show() {}
fn wire() { edge_routes![r#type::show]; }
",
);
scan.crate_name = Some("type".to_owned());
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
assert_eq!(scan.registered_fns().len(), 1);
}
#[test]
fn a_raw_identifier_modules_conventional_file_still_matches_its_registration() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("src")).unwrap();
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"my-app\"\nversion = \"0.1.0\"\n",
)
.unwrap();
std::fs::write(
dir.path().join("src/lib.rs"),
"mod r#type;\nfn wire() { edge_routes![my_app::r#type::show]; }\n",
)
.unwrap();
std::fs::write(
dir.path().join("src/type.rs"),
"#[edge]\npub fn show() {}\n",
)
.unwrap();
let scan = resolve_edge_scan(dir.path());
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
assert_eq!(scan.registered_fns().len(), 1);
}
#[test]
fn a_leading_double_colon_qualified_registration_matches_its_module_path() {
let mut scan = scan_one(
r"
#[edge]
pub fn show() {}
fn wire() { edge_routes![::edgeapp::show]; }
",
);
scan.crate_name = Some("edgeapp".to_owned());
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
assert_eq!(scan.registered_fns().len(), 1);
}
#[test]
fn an_unrelated_qualifier_does_not_match_even_with_a_crate_name_set() {
let mut scan = scan_one(
r"
#[edge]
pub fn show() {}
fn wire() { edge_routes![otherpkg::show]; }
",
);
scan.crate_name = Some("edgeapp".to_owned());
assert_eq!(
scan.registered_fns().len(),
0,
"{:?}",
scan.registered_fns()
);
}
#[test]
fn a_relative_child_module_registration_is_left_unresolved() {
let scan = scan_one(
r"
mod api {
mod v1 {
#[edge]
pub fn show() {}
}
fn wire() { edge_routes![v1::show]; }
}
",
);
let unregistered: Vec<&EdgeFn> = scan.unregistered();
assert_eq!(unregistered.len(), 1, "{unregistered:?}");
assert_eq!(
unregistered[0].module_path,
vec!["api".to_owned(), "v1".to_owned()]
);
assert!(scan.registered_fns().is_empty());
}
#[test]
fn a_glob_reexported_registration_is_left_unresolved() {
let scan = scan_one(
r"
mod handlers {
#[edge]
pub fn show() {}
}
mod api {
pub use crate::handlers::*;
}
fn wire() { edge_routes![api::show]; }
",
);
let unregistered: Vec<&EdgeFn> = scan.unregistered();
assert_eq!(unregistered.len(), 1, "{unregistered:?}");
assert_eq!(unregistered[0].module_path, vec!["handlers".to_owned()]);
assert!(scan.registered_fns().is_empty());
}
#[test]
fn a_relative_child_module_registration_does_not_shadow_a_sibling() {
let scan = scan_one(
r"
mod a {
mod x {
#[edge]
pub fn f() {}
}
}
mod b {
mod x {
#[edge]
pub fn f() {}
}
fn wire() { edge_routes![x::f]; }
}
",
);
assert_eq!(scan.unregistered().len(), 2, "{:?}", scan.unregistered());
assert!(
scan.registered_fns().is_empty(),
"{:?}",
scan.registered_fns()
);
}
#[test]
fn module_path_is_derived_from_an_out_of_line_modules_file() {
let scan = scan_sources(&[
("src/users.rs", "#[edge]\npub fn show() {}\n"),
("src/admin.rs", "#[edge]\npub fn show() {}\n"),
(
"src/lib.rs",
"mod users; mod admin; fn wire() { edge_routes![users::show]; }",
),
]);
let unregistered: Vec<&EdgeFn> = scan.unregistered();
assert_eq!(unregistered.len(), 1, "{unregistered:?}");
assert_eq!(unregistered[0].name, "show");
assert_eq!(unregistered[0].module_path, vec!["admin".to_owned()]);
let registered = scan.registered_fns();
assert_eq!(registered.len(), 1, "{registered:?}");
assert_eq!(registered[0].name, "show");
assert_eq!(registered[0].module_path, vec!["users".to_owned()]);
}
#[test]
fn a_flat_bin_target_file_is_its_own_crate_root() {
let scan = scan_sources(&[(
"src/bin/edge-capsule.rs",
"#[edge]\npub fn show() {}\nfn main() { edge_routes![crate::show]; }\n",
)]);
assert_eq!(scan.functions[0].module_path, Vec::<String>::new());
assert_eq!(scan.functions[0].crate_root, "bin:edge-capsule");
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
}
#[test]
fn crate_qualified_registrations_do_not_cross_a_bin_targets_crate_boundary() {
let scan = scan_sources(&[
("src/lib.rs", "#[edge]\npub fn show() {}\n"),
(
"src/bin/edge-capsule.rs",
"#[edge]\npub fn show() {}\nfn main() { edge_routes![crate::show]; }\n",
),
]);
let unregistered: Vec<&EdgeFn> = scan.unregistered();
assert_eq!(unregistered.len(), 1, "{unregistered:?}");
assert_eq!(unregistered[0].file, "src/lib.rs");
let registered = scan.registered_fns();
assert_eq!(registered.len(), 1, "{registered:?}");
assert_eq!(registered[0].file, "src/bin/edge-capsule.rs");
}
#[test]
fn crate_qualified_registrations_do_not_cross_the_default_binarys_crate_boundary() {
let scan = scan_sources(&[
("src/lib.rs", "#[edge]\npub fn show() {}\n"),
(
"src/main.rs",
"#[edge]\npub fn show() {}\nfn main() { edge_routes![crate::show]; }\n",
),
]);
let unregistered: Vec<&EdgeFn> = scan.unregistered();
assert_eq!(unregistered.len(), 1, "{unregistered:?}");
assert_eq!(unregistered[0].file, "src/lib.rs");
let registered = scan.registered_fns();
assert_eq!(registered.len(), 1, "{registered:?}");
assert_eq!(registered[0].file, "src/main.rs");
}
#[test]
fn a_crate_name_qualified_registration_does_not_match_the_default_binarys_own_function() {
let mut scan = scan_sources(&[
("src/lib.rs", "#[edge]\npub fn show() {}\n"),
(
"src/main.rs",
"#[edge]\npub fn show() {}\nfn main() { edge_routes![edgeapp::show]; }\n",
),
]);
scan.crate_name = Some("edgeapp".to_owned());
let unregistered: Vec<&EdgeFn> = scan.unregistered();
assert_eq!(unregistered.len(), 1, "{unregistered:?}");
assert_eq!(unregistered[0].file, "src/main.rs");
let registered = scan.registered_fns();
assert_eq!(registered.len(), 1, "{registered:?}");
assert_eq!(registered[0].file, "src/lib.rs");
}
#[test]
fn resolve_edge_scan_gives_src_main_rs_its_own_crate_identity() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("src")).unwrap();
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"demo\"\nversion = \"0.1.0\"\n",
)
.unwrap();
std::fs::write(dir.path().join("src/lib.rs"), "#[edge]\npub fn show() {}\n").unwrap();
std::fs::write(
dir.path().join("src/main.rs"),
"#[edge]\npub fn show() {}\nfn main() { edge_routes![crate::show]; }\n",
)
.unwrap();
let scan = resolve_edge_scan(dir.path());
let unregistered: Vec<&EdgeFn> = scan.unregistered();
assert_eq!(unregistered.len(), 1, "{unregistered:?}");
assert_eq!(unregistered[0].file, "src/lib.rs");
}
#[test]
fn resolve_edge_scan_honors_a_custom_lib_path_as_the_crate_root() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("src")).unwrap();
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"my-app\"\nversion = \"0.1.0\"\n\n[lib]\npath = \"src/app.rs\"\n",
)
.unwrap();
std::fs::write(
dir.path().join("src/app.rs"),
"#[edge]\npub fn show() {}\nfn wire() { edge_routes![my_app::show]; }\n",
)
.unwrap();
let scan = resolve_edge_scan(dir.path());
assert_eq!(scan.functions[0].module_path, Vec::<String>::new());
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
}
#[test]
fn resolve_edge_scan_resolves_a_nested_custom_lib_paths_own_submodule() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("src/custom")).unwrap();
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"my-app\"\nversion = \"0.1.0\"\n\n[lib]\npath = \"src/custom/app.rs\"\n",
)
.unwrap();
std::fs::write(dir.path().join("src/custom/app.rs"), "mod routes;\n").unwrap();
std::fs::write(
dir.path().join("src/custom/routes.rs"),
"#[edge]\npub fn show() {}\nfn wire() { edge_routes![my_app::routes::show]; }\n",
)
.unwrap();
let scan = resolve_edge_scan(dir.path());
assert_eq!(scan.functions.len(), 1, "{:?}", scan.functions);
assert_eq!(scan.functions[0].module_path, vec!["routes".to_owned()]);
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
}
#[test]
fn resolve_edge_scan_follows_a_path_attribute_in_a_custom_lib_paths_crate_tree() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("src")).unwrap();
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"my-app\"\nversion = \"0.1.0\"\n\n[lib]\npath = \"src/app.rs\"\n",
)
.unwrap();
std::fs::write(
dir.path().join("src/app.rs"),
"#[path = \"actual.rs\"]\nmod handlers;\nfn wire() { edge_routes![my_app::handlers::show]; }\n",
)
.unwrap();
std::fs::write(
dir.path().join("src/actual.rs"),
"#[edge]\npub fn show() {}\n",
)
.unwrap();
let scan = resolve_edge_scan(dir.path());
assert_eq!(scan.functions.len(), 1, "{:?}", scan.functions);
assert_eq!(scan.functions[0].module_path, vec!["handlers".to_owned()]);
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
}
#[test]
fn resolve_edge_scan_treats_an_unreal_src_bin_file_as_an_ordinary_library_submodule_when_autobins_is_off()
{
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("src/bin")).unwrap();
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"my-app\"\nversion = \"0.1.0\"\nautobins = false\n",
)
.unwrap();
std::fs::write(
dir.path().join("src/lib.rs"),
r"
mod bin {
mod foo;
}
fn wire() { edge_routes![crate::bin::foo::show]; }
",
)
.unwrap();
std::fs::write(
dir.path().join("src/bin/foo.rs"),
"#[edge]\npub fn show() {}\n",
)
.unwrap();
let scan = resolve_edge_scan(dir.path());
assert_eq!(scan.functions.len(), 1, "{:?}", scan.functions);
assert_eq!(scan.functions[0].crate_root, "");
assert_eq!(
scan.functions[0].module_path,
vec!["bin".to_owned(), "foo".to_owned()]
);
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
}
#[test]
fn resolve_edge_scan_still_recognizes_an_explicitly_declared_bin_when_autobins_is_off() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("src/bin")).unwrap();
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"my-app\"\nversion = \"0.1.0\"\nautobins = false\n\n\
[[bin]]\nname = \"foo\"\n",
)
.unwrap();
std::fs::write(
dir.path().join("src/bin/foo.rs"),
"#[edge]\npub fn show() {}\nfn wire() { edge_routes![show]; }\n",
)
.unwrap();
let scan = resolve_edge_scan(dir.path());
assert_eq!(scan.functions.len(), 1, "{:?}", scan.functions);
assert_eq!(scan.functions[0].crate_root, "bin:foo");
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
}
#[test]
fn resolve_edge_scan_does_not_treat_src_main_as_its_own_crate_when_autobins_is_off_and_undeclared()
{
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("src")).unwrap();
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"my-app\"\nversion = \"0.1.0\"\nautobins = false\n",
)
.unwrap();
std::fs::write(
dir.path().join("src/main.rs"),
"#[edge]\npub fn show() {}\nfn wire() { edge_routes![crate::show]; }\n",
)
.unwrap();
let scan = resolve_edge_scan(dir.path());
assert_eq!(scan.functions.len(), 1, "{:?}", scan.functions);
assert_eq!(scan.functions[0].crate_root, "", "{:?}", scan.functions);
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
}
#[test]
fn resolve_edge_scan_resolves_a_path_attribute_out_of_line_modules_real_name() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("src")).unwrap();
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"my-app\"\nversion = \"0.1.0\"\n",
)
.unwrap();
std::fs::write(
dir.path().join("src/lib.rs"),
"#[path = \"actual.rs\"]\nmod handlers;\nfn wire() { edge_routes![crate::handlers::show]; }\n",
)
.unwrap();
std::fs::write(
dir.path().join("src/actual.rs"),
"#[edge]\npub fn show() {}\n",
)
.unwrap();
let scan = resolve_edge_scan(dir.path());
assert_eq!(scan.functions.len(), 1, "{:?}", scan.functions);
assert_eq!(scan.functions[0].module_path, vec!["handlers".to_owned()]);
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
}
#[test]
fn resolve_edge_scan_resolves_a_path_attribute_mod_nested_inside_an_inline_module() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("src/api")).unwrap();
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"my-app\"\nversion = \"0.1.0\"\n",
)
.unwrap();
std::fs::write(
dir.path().join("src/lib.rs"),
r#"
mod api {
#[path = "actual.rs"]
mod handlers;
}
fn wire() { edge_routes![crate::api::handlers::show]; }
"#,
)
.unwrap();
std::fs::write(
dir.path().join("src/api/actual.rs"),
"#[edge]\npub fn show() {}\n",
)
.unwrap();
let scan = resolve_edge_scan(dir.path());
assert_eq!(scan.functions.len(), 1, "{:?}", scan.functions);
assert_eq!(
scan.functions[0].module_path,
vec!["api".to_owned(), "handlers".to_owned()]
);
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
}
#[test]
fn resolve_edge_scan_resolves_a_path_attribute_mod_nested_inside_an_inline_module_of_an_out_of_line_file()
{
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("src/foo/inline")).unwrap();
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"my-app\"\nversion = \"0.1.0\"\n",
)
.unwrap();
std::fs::write(dir.path().join("src/lib.rs"), "mod foo;\n").unwrap();
std::fs::write(
dir.path().join("src/foo.rs"),
r#"
mod inline {
#[path = "actual.rs"]
mod handlers;
}
fn wire() { edge_routes![crate::foo::inline::handlers::show]; }
"#,
)
.unwrap();
std::fs::write(
dir.path().join("src/foo/inline/actual.rs"),
"#[edge]\npub fn show() {}\n",
)
.unwrap();
let scan = resolve_edge_scan(dir.path());
assert_eq!(scan.functions.len(), 1, "{:?}", scan.functions);
assert_eq!(
scan.functions[0].module_path,
vec!["foo".to_owned(), "inline".to_owned(), "handlers".to_owned()]
);
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
}
#[test]
fn resolve_edge_scan_honors_a_dot_component_custom_lib_path() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("src")).unwrap();
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"my-app\"\nversion = \"0.1.0\"\n\n[lib]\npath = \"./src/app.rs\"\n",
)
.unwrap();
std::fs::write(
dir.path().join("src/app.rs"),
"#[edge]\npub fn show() {}\nfn wire() { edge_routes![my_app::show]; }\n",
)
.unwrap();
let scan = resolve_edge_scan(dir.path());
assert_eq!(scan.functions.len(), 1, "{:?}", scan.functions);
assert_eq!(scan.functions[0].module_path, Vec::<String>::new());
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
}
#[test]
fn resolve_edge_scan_honors_a_dot_dot_component_custom_lib_path() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("src/sub")).unwrap();
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"my-app\"\nversion = \"0.1.0\"\n\n[lib]\npath = \"src/sub/../app.rs\"\n",
)
.unwrap();
std::fs::write(
dir.path().join("src/app.rs"),
"#[edge]\npub fn show() {}\nfn wire() { edge_routes![my_app::show]; }\n",
)
.unwrap();
let scan = resolve_edge_scan(dir.path());
assert_eq!(scan.functions.len(), 1, "{:?}", scan.functions);
assert_eq!(scan.functions[0].module_path, Vec::<String>::new());
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
}
#[test]
fn resolve_edge_scan_honors_a_custom_lib_path_outside_src() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("lib")).unwrap();
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"my-app\"\nversion = \"0.1.0\"\n\n[lib]\npath = \"lib/app.rs\"\n",
)
.unwrap();
std::fs::write(
dir.path().join("lib/app.rs"),
"#[edge]\npub fn show() {}\nfn wire() { edge_routes![my_app::show]; }\n",
)
.unwrap();
let scan = resolve_edge_scan(dir.path());
assert_eq!(scan.functions.len(), 1, "{:?}", scan.functions);
assert_eq!(scan.functions[0].module_path, Vec::<String>::new());
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
}
#[test]
fn resolve_edge_scan_excludes_the_inactive_conventional_lib_source() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("src")).unwrap();
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"my-app\"\nversion = \"0.1.0\"\n\n[lib]\npath = \"src/app.rs\"\n",
)
.unwrap();
std::fs::write(
dir.path().join("src/app.rs"),
"#[edge]\npub fn show() {}\nfn wire() { edge_routes![my_app::show]; }\n",
)
.unwrap();
std::fs::write(
dir.path().join("src/lib.rs"),
"#[edge]\npub fn stale() {}\n",
)
.unwrap();
let scan = resolve_edge_scan(dir.path());
assert_eq!(scan.functions.len(), 1, "{:?}", scan.functions);
assert_eq!(scan.functions[0].name, "show");
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
}
#[test]
fn resolve_edge_scan_with_extra_file_honors_a_custom_lib_path_outside_src_alongside_a_custom_bin_path()
{
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("lib")).unwrap();
std::fs::create_dir_all(dir.path().join("cmd")).unwrap();
std::fs::write(
dir.path().join("Cargo.toml"),
r#"
[package]
name = "demo"
version = "0.1.0"
[lib]
path = "lib/app.rs"
[[bin]]
name = "edge-capsule"
path = "cmd/edge.rs"
"#,
)
.unwrap();
std::fs::write(dir.path().join("lib/app.rs"), "#[edge]\npub fn show() {}\n").unwrap();
std::fs::write(
dir.path().join("cmd/edge.rs"),
"#[edge]\npub fn show() {}\nfn main() { edge_routes![crate::show]; }\n",
)
.unwrap();
let scan = resolve_edge_scan_with_extra_file(
dir.path(),
&[],
Some(&dir.path().join("cmd/edge.rs")),
);
let unregistered = scan.unregistered();
assert_eq!(unregistered.len(), 1, "{unregistered:?}");
assert_eq!(unregistered[0].file, "lib/app.rs");
let registered = scan.registered_fns();
assert_eq!(registered.len(), 1, "{registered:?}");
assert_eq!(registered[0].file, "cmd/edge.rs");
}
#[test]
fn a_bare_registration_crosses_a_bin_targets_crate_boundary() {
let scan = scan_sources(&[
("src/lib.rs", "#[edge]\npub fn show() {}\n"),
(
"src/bin/edge-capsule.rs",
"fn main() { edge_routes![show]; }\n",
),
]);
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
assert_eq!(scan.registered_fns().len(), 1);
}
#[test]
fn a_directory_style_bin_targets_own_submodule_is_relative_to_its_root() {
let scan = scan_sources(&[
(
"src/bin/edge-capsule/main.rs",
"mod routes; fn main() { edge_routes![routes::show]; }\n",
),
(
"src/bin/edge-capsule/routes.rs",
"#[edge]\npub fn show() {}\n",
),
]);
assert_eq!(scan.functions[0].module_path, vec!["routes".to_owned()]);
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
}
#[test]
fn a_custom_out_of_tree_bin_path_is_its_own_crate_root() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("src")).unwrap();
std::fs::create_dir_all(dir.path().join("cmd")).unwrap();
std::fs::write(
dir.path().join("Cargo.toml"),
r#"
[package]
name = "demo"
version = "0.1.0"
[[bin]]
name = "edge-capsule"
path = "cmd/edge.rs"
"#,
)
.unwrap();
std::fs::write(dir.path().join("src/lib.rs"), "#[edge]\npub fn show() {}\n").unwrap();
std::fs::write(
dir.path().join("cmd/edge.rs"),
"#[edge]\npub fn show() {}\nfn main() { edge_routes![crate::show]; }\n",
)
.unwrap();
let scan = resolve_edge_scan_with_extra_file(
dir.path(),
&[],
Some(&dir.path().join("cmd/edge.rs")),
);
let unregistered = scan.unregistered();
assert_eq!(unregistered.len(), 1, "{unregistered:?}");
assert_eq!(unregistered[0].file, "src/lib.rs");
let registered = scan.registered_fns();
assert_eq!(registered.len(), 1, "{registered:?}");
assert_eq!(registered[0].file, "cmd/edge.rs");
}
#[test]
fn a_custom_bin_path_nested_under_src_bin_is_its_own_crate_root() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("src/bin/tools")).unwrap();
std::fs::write(
dir.path().join("Cargo.toml"),
r#"
[package]
name = "demo"
version = "0.1.0"
[[bin]]
name = "edge-capsule"
path = "src/bin/tools/capsule.rs"
"#,
)
.unwrap();
std::fs::write(
dir.path().join("src/bin/tools/capsule.rs"),
"#[edge]\npub fn show() {}\nfn main() { edge_routes![crate::show]; }\n",
)
.unwrap();
let scan = resolve_edge_scan_with_extra_file(
dir.path(),
&[],
Some(&dir.path().join("src/bin/tools/capsule.rs")),
);
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
let registered = scan.registered_fns();
assert_eq!(registered.len(), 1, "{registered:?}");
assert_eq!(registered[0].file, "src/bin/tools/capsule.rs");
}
#[test]
fn a_conventional_flat_capsule_roots_own_submodule_is_scanned() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("src/bin")).unwrap();
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"demo\"\nversion = \"0.1.0\"\nautobins = false\n",
)
.unwrap();
std::fs::write(
dir.path().join("src/bin/edge-capsule.rs"),
"mod routes;\nfn main() { edge_routes![crate::routes::show]; }\n",
)
.unwrap();
std::fs::write(
dir.path().join("src/bin/routes.rs"),
"#[edge]\npub fn show() {}\n",
)
.unwrap();
let scan = resolve_edge_scan_with_extra_file(
dir.path(),
&[],
Some(&dir.path().join("src/bin/edge-capsule.rs")),
);
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
let registered = scan.registered_fns();
assert_eq!(registered.len(), 1, "{registered:?}");
assert_eq!(registered[0].file, "src/bin/routes.rs");
assert_eq!(registered[0].module_path, vec!["routes".to_owned()]);
}
#[test]
fn a_custom_in_src_bin_path_is_its_own_crate_root() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("src")).unwrap();
std::fs::write(
dir.path().join("Cargo.toml"),
r#"
[package]
name = "demo"
version = "0.1.0"
[[bin]]
name = "edge-capsule"
path = "src/edge.rs"
"#,
)
.unwrap();
std::fs::write(dir.path().join("src/lib.rs"), "#[edge]\npub fn show() {}\n").unwrap();
std::fs::write(
dir.path().join("src/edge.rs"),
"#[edge]\npub fn show() {}\nfn main() { edge_routes![crate::show]; }\n",
)
.unwrap();
let scan = resolve_edge_scan_with_extra_file(
dir.path(),
&[],
Some(&dir.path().join("src/edge.rs")),
);
let unregistered = scan.unregistered();
assert_eq!(unregistered.len(), 1, "{unregistered:?}");
assert_eq!(unregistered[0].file, "src/lib.rs");
let registered = scan.registered_fns();
assert_eq!(registered.len(), 1, "{registered:?}");
assert_eq!(registered[0].file, "src/edge.rs");
assert_eq!(registered[0].module_path, Vec::<String>::new());
}
#[test]
fn a_custom_out_of_tree_bins_out_of_line_submodule_is_scanned() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("src")).unwrap();
std::fs::create_dir_all(dir.path().join("cmd")).unwrap();
std::fs::write(
dir.path().join("Cargo.toml"),
r#"
[package]
name = "demo"
version = "0.1.0"
[[bin]]
name = "edge-capsule"
path = "cmd/edge.rs"
"#,
)
.unwrap();
std::fs::write(
dir.path().join("cmd/edge.rs"),
"mod routes;\nfn main() { edge_routes![routes::show]; }\n",
)
.unwrap();
std::fs::write(
dir.path().join("cmd/routes.rs"),
"#[edge]\npub fn show() {}\n",
)
.unwrap();
let scan = resolve_edge_scan_with_extra_file(
dir.path(),
&[],
Some(&dir.path().join("cmd/edge.rs")),
);
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
let registered = scan.registered_fns();
assert_eq!(registered.len(), 1, "{registered:?}");
assert_eq!(registered[0].file, "cmd/routes.rs");
assert_eq!(registered[0].module_path, vec!["routes".to_owned()]);
assert_eq!(registered[0].crate_root, "bin:edge-capsule");
}
#[test]
fn a_custom_out_of_tree_bins_raw_identifier_submodule_is_scanned() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("src")).unwrap();
std::fs::create_dir_all(dir.path().join("cmd")).unwrap();
std::fs::write(
dir.path().join("Cargo.toml"),
r#"
[package]
name = "demo"
version = "0.1.0"
[[bin]]
name = "edge-capsule"
path = "cmd/edge.rs"
"#,
)
.unwrap();
std::fs::write(
dir.path().join("cmd/edge.rs"),
"mod r#type;\nfn main() { edge_routes![r#type::show]; }\n",
)
.unwrap();
std::fs::write(
dir.path().join("cmd/type.rs"),
"#[edge]\npub fn show() {}\n",
)
.unwrap();
let scan = resolve_edge_scan_with_extra_file(
dir.path(),
&[],
Some(&dir.path().join("cmd/edge.rs")),
);
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
let registered = scan.registered_fns();
assert_eq!(registered.len(), 1, "{registered:?}");
assert_eq!(registered[0].file, "cmd/type.rs");
}
#[test]
fn cfg_false_if_let_struct_pattern_does_not_leak_its_body_as_a_separate_statement() {
let scan = scan_one_with_features(
r#"
#[edge]
pub fn show() {}
struct Foo {
x: i32,
}
fn value() -> Foo {
Foo { x: 1 }
}
fn wire() {
#[cfg(feature = "premium")]
if let Foo { x } = value() {
edge_routes![crate::show];
}
}
"#,
&[],
);
assert!(
scan.registered_fns().is_empty(),
"{:?}",
scan.registered_fns()
);
assert_eq!(scan.unregistered().len(), 1, "{:?}", scan.unregistered());
assert_eq!(scan.unregistered()[0].name, "show");
}
#[test]
fn cfg_false_for_loop_struct_pattern_does_not_leak_its_body_as_a_separate_statement() {
let scan = scan_one_with_features(
r#"
#[edge]
pub fn show() {}
struct Foo {
x: i32,
}
fn items() -> Vec<Foo> {
Vec::new()
}
fn wire() {
#[cfg(feature = "premium")]
for Foo { x } in items() {
edge_routes![crate::show];
}
}
"#,
&[],
);
assert!(
scan.registered_fns().is_empty(),
"{:?}",
scan.registered_fns()
);
assert_eq!(scan.unregistered().len(), 1, "{:?}", scan.unregistered());
assert_eq!(scan.unregistered()[0].name, "show");
}
#[test]
fn cfg_false_extern_block_without_unsafe_does_not_swallow_the_following_registration() {
let scan = scan_one_with_features(
r#"
#[edge]
pub fn show() {}
fn wire() {
#[cfg(feature = "premium")]
extern "C" {
fn premium_only();
}
edge_routes![crate::show];
}
"#,
&[],
);
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
assert_eq!(
scan.registered_fns().len(),
1,
"{:?}",
scan.registered_fns()
);
assert_eq!(scan.registered_fns()[0].name, "show");
}
#[test]
fn cfg_false_extern_block_with_unsafe_does_not_swallow_the_following_registration() {
let scan = scan_one_with_features(
r#"
#[edge]
pub fn show() {}
fn wire() {
#[cfg(feature = "premium")]
unsafe extern "C" {
fn premium_only();
}
edge_routes![crate::show];
}
"#,
&[],
);
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
assert_eq!(
scan.registered_fns().len(),
1,
"{:?}",
scan.registered_fns()
);
assert_eq!(scan.registered_fns()[0].name, "show");
}
#[test]
fn cfg_false_if_let_or_pattern_does_not_leak_its_body_as_a_separate_statement() {
let scan = scan_one_with_features(
r#"
#[edge]
pub fn show() {}
enum E {
A { x: i32 },
B { x: i32 },
}
fn value() -> E {
E::A { x: 1 }
}
fn wire() {
#[cfg(feature = "premium")]
if let E::A { x } | E::B { x } = value() {
edge_routes![crate::show];
}
}
"#,
&[],
);
assert!(
scan.registered_fns().is_empty(),
"{:?}",
scan.registered_fns()
);
assert_eq!(scan.unregistered().len(), 1, "{:?}", scan.unregistered());
assert_eq!(scan.unregistered()[0].name, "show");
}
#[test]
fn cfg_false_bodyless_trait_method_with_return_type_does_not_swallow_the_next_methods_body() {
let scan = scan_one_with_features(
r#"
#[edge]
pub fn show() {}
trait Wiring {
#[cfg(feature = "premium")]
fn disabled() -> ();
fn routes() {
edge_routes![crate::show];
}
}
"#,
&[],
);
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
assert_eq!(
scan.registered_fns().len(),
1,
"{:?}",
scan.registered_fns()
);
assert_eq!(scan.registered_fns()[0].name, "show");
}
#[test]
fn resolve_edge_scan_resolves_a_nested_path_attribute_in_a_custom_capsule_root() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("cmd")).unwrap();
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"my-app\"\nversion = \"0.1.0\"\n\n\
[[bin]]\nname = \"edge-capsule\"\npath = \"cmd/edge.rs\"\n",
)
.unwrap();
std::fs::write(
dir.path().join("cmd/edge.rs"),
"mod wiring;\nfn main() {}\n",
)
.unwrap();
std::fs::write(
dir.path().join("cmd/wiring.rs"),
"#[path = \"actual.rs\"]\nmod handlers;\nfn wire() { edge_routes![crate::wiring::handlers::show]; }\n",
)
.unwrap();
std::fs::write(
dir.path().join("cmd/actual.rs"),
"#[edge]\npub fn show() {}\n",
)
.unwrap();
let scan = resolve_edge_scan_with_extra_file(
dir.path(),
&[],
Some(&dir.path().join("cmd/edge.rs")),
);
assert_eq!(scan.functions.len(), 1, "{:?}", scan.functions);
assert_eq!(scan.functions[0].file, "cmd/actual.rs");
assert_eq!(
scan.functions[0].module_path,
vec!["wiring".to_owned(), "handlers".to_owned()]
);
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
}
#[test]
fn resolve_edge_scan_expands_a_path_attribute_introduced_by_cfg_attr() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("src")).unwrap();
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"my-app\"\nversion = \"0.1.0\"\n\n[features]\nalt = []\n",
)
.unwrap();
std::fs::write(
dir.path().join("src/lib.rs"),
r#"
#[cfg_attr(feature = "alt", path = "actual.rs")]
mod handlers;
fn wire() { edge_routes![crate::handlers::show]; }
"#,
)
.unwrap();
std::fs::write(
dir.path().join("src/actual.rs"),
"#[edge]\npub fn show() {}\n",
)
.unwrap();
let scan = resolve_edge_scan_with_extra_file(dir.path(), &["alt"], None);
assert_eq!(scan.functions.len(), 1, "{:?}", scan.functions);
assert_eq!(scan.functions[0].file, "src/actual.rs");
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
}
#[test]
fn resolve_edge_scan_scans_a_path_attributes_target_outside_src() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("src")).unwrap();
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"my-app\"\nversion = \"0.1.0\"\n",
)
.unwrap();
std::fs::write(
dir.path().join("src/lib.rs"),
r#"
#[path = "../routes.rs"]
mod handlers;
fn wire() { edge_routes![crate::handlers::show]; }
"#,
)
.unwrap();
std::fs::write(dir.path().join("routes.rs"), "#[edge]\npub fn show() {}\n").unwrap();
let scan = resolve_edge_scan(dir.path());
assert_eq!(scan.functions.len(), 1, "{:?}", scan.functions);
assert_eq!(scan.functions[0].file, "routes.rs");
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
}
#[test]
fn resolve_edge_scan_follows_a_path_attributes_targets_own_child_module() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("src")).unwrap();
std::fs::create_dir_all(dir.path().join("outside")).unwrap();
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"my-app\"\nversion = \"0.1.0\"\n",
)
.unwrap();
std::fs::write(
dir.path().join("src/lib.rs"),
r#"
#[path = "../outside/shared.rs"]
mod handlers;
fn wire() { edge_routes![crate::handlers::child::show]; }
"#,
)
.unwrap();
std::fs::write(dir.path().join("outside/shared.rs"), "pub mod child;\n").unwrap();
std::fs::write(
dir.path().join("outside/child.rs"),
"#[edge]\npub fn show() {}\n",
)
.unwrap();
let scan = resolve_edge_scan(dir.path());
assert_eq!(scan.functions.len(), 1, "{:?}", scan.functions);
assert_eq!(scan.functions[0].file, "outside/child.rs");
assert_eq!(
scan.functions[0].module_path,
vec!["handlers".to_string(), "child".to_string()]
);
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
}
#[test]
fn resolve_edge_scan_preserves_every_identity_for_a_shared_path_attribute_file() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("src")).unwrap();
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"my-app\"\nversion = \"0.1.0\"\n",
)
.unwrap();
std::fs::write(
dir.path().join("src/lib.rs"),
r#"
#[path = "shared.rs"]
mod a;
#[path = "shared.rs"]
mod b;
fn wire() {
edge_routes![crate::a::show];
edge_routes![crate::b::show];
}
"#,
)
.unwrap();
std::fs::write(
dir.path().join("src/shared.rs"),
"#[edge]\npub fn show() {}\n",
)
.unwrap();
let scan = resolve_edge_scan(dir.path());
assert_eq!(scan.functions.len(), 2, "{:?}", scan.functions);
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
let module_paths: BTreeSet<Vec<String>> = scan
.functions
.iter()
.map(|f| f.module_path.clone())
.collect();
assert!(
module_paths.contains(&vec!["a".to_owned()]),
"{module_paths:?}"
);
assert!(
module_paths.contains(&vec!["b".to_owned()]),
"{module_paths:?}"
);
}
#[test]
fn cfg_false_qualified_brace_delimited_macro_invocation_does_not_swallow_the_following_registration()
{
let scan = scan_one_with_features(
r#"
#[edge]
pub fn show() {}
macro_rules! configure {
($($t:tt)*) => {};
}
fn wire() {
#[cfg(feature = "premium")]
crate::configure! { a b c }
edge_routes![crate::show];
}
"#,
&[],
);
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
assert_eq!(
scan.registered_fns().len(),
1,
"{:?}",
scan.registered_fns()
);
assert_eq!(scan.registered_fns()[0].name, "show");
}
#[test]
fn resolve_edge_scan_expands_a_path_attribute_introduced_by_a_nested_cfg_attr() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("src")).unwrap();
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"my-app\"\nversion = \"0.1.0\"\n\n[features]\na = []\nb = []\n",
)
.unwrap();
std::fs::write(
dir.path().join("src/lib.rs"),
r#"
#[cfg_attr(feature = "a", cfg_attr(feature = "b", path = "actual.rs"))]
mod handlers;
fn wire() { edge_routes![crate::handlers::show]; }
"#,
)
.unwrap();
std::fs::write(
dir.path().join("src/actual.rs"),
"#[edge]\npub fn show() {}\n",
)
.unwrap();
let scan = resolve_edge_scan_with_extra_file(dir.path(), &["a", "b"], None);
assert_eq!(scan.functions.len(), 1, "{:?}", scan.functions);
assert_eq!(scan.functions[0].file, "src/actual.rs");
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
}
#[test]
fn cfg_false_match_arm_does_not_swallow_the_following_arms_registration() {
let scan = scan_one_with_features(
r#"
#[edge]
pub fn show() {}
fn wire(x: bool) {
match x {
#[cfg(feature = "premium")]
true => edge_routes![crate::premium_thing],
_ => edge_routes![crate::show],
}
}
"#,
&[],
);
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
assert_eq!(
scan.registered_fns().len(),
1,
"{:?}",
scan.registered_fns()
);
assert_eq!(scan.registered_fns()[0].name, "show");
}
#[test]
fn resolve_edge_scan_preserves_every_identity_for_a_shared_path_file_in_a_custom_crate_tree() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("cmd")).unwrap();
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"my-app\"\nversion = \"0.1.0\"\n\n\
[[bin]]\nname = \"edge-capsule\"\npath = \"cmd/edge.rs\"\n",
)
.unwrap();
std::fs::write(
dir.path().join("cmd/edge.rs"),
r#"
#[path = "shared.rs"]
mod a;
#[path = "shared.rs"]
mod b;
fn main() {
edge_routes![crate::a::show];
edge_routes![crate::b::show];
}
"#,
)
.unwrap();
std::fs::write(
dir.path().join("cmd/shared.rs"),
"#[edge]\npub fn show() {}\n",
)
.unwrap();
let scan = resolve_edge_scan_with_extra_file(
dir.path(),
&[],
Some(&dir.path().join("cmd/edge.rs")),
);
assert_eq!(scan.functions.len(), 2, "{:?}", scan.functions);
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
let module_paths: BTreeSet<Vec<String>> = scan
.functions
.iter()
.map(|f| f.module_path.clone())
.collect();
assert!(
module_paths.contains(&vec!["a".to_owned()]),
"{module_paths:?}"
);
assert!(
module_paths.contains(&vec!["b".to_owned()]),
"{module_paths:?}"
);
}
#[test]
fn resolve_edge_scan_traverses_a_shared_path_files_children_for_every_alias() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("cmd")).unwrap();
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"my-app\"\nversion = \"0.1.0\"\n\n\
[[bin]]\nname = \"edge-capsule\"\npath = \"cmd/edge.rs\"\n",
)
.unwrap();
std::fs::write(
dir.path().join("cmd/edge.rs"),
r#"
#[path = "shared.rs"]
mod a;
#[path = "shared.rs"]
mod b;
fn main() {
edge_routes![crate::a::child::show];
edge_routes![crate::b::child::show];
}
"#,
)
.unwrap();
std::fs::write(
dir.path().join("cmd/shared.rs"),
"#[path = \"child.rs\"]\npub mod child;\n",
)
.unwrap();
std::fs::write(
dir.path().join("cmd/child.rs"),
"#[edge]\npub fn show() {}\n",
)
.unwrap();
let scan = resolve_edge_scan_with_extra_file(
dir.path(),
&[],
Some(&dir.path().join("cmd/edge.rs")),
);
assert_eq!(scan.functions.len(), 2, "{:?}", scan.functions);
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
let module_paths: BTreeSet<Vec<String>> = scan
.functions
.iter()
.map(|f| f.module_path.clone())
.collect();
assert!(
module_paths.contains(&vec!["a".to_owned(), "child".to_owned()]),
"{module_paths:?}"
);
assert!(
module_paths.contains(&vec!["b".to_owned(), "child".to_owned()]),
"{module_paths:?}"
);
}
#[test]
fn resolve_edge_scan_terminates_on_a_circular_path_attribute() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("cmd")).unwrap();
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"my-app\"\nversion = \"0.1.0\"\n\n\
[[bin]]\nname = \"edge-capsule\"\npath = \"cmd/edge.rs\"\n",
)
.unwrap();
std::fs::write(
dir.path().join("cmd/edge.rs"),
"#[path = \"a.rs\"]\nmod a;\nfn main() {}\n",
)
.unwrap();
std::fs::write(dir.path().join("cmd/a.rs"), "#[path = \"b.rs\"]\nmod b;\n").unwrap();
std::fs::write(dir.path().join("cmd/b.rs"), "#[path = \"a.rs\"]\nmod a;\n").unwrap();
let scan = resolve_edge_scan_with_extra_file(
dir.path(),
&[],
Some(&dir.path().join("cmd/edge.rs")),
);
assert!(scan.functions.is_empty(), "{:?}", scan.functions);
}
#[test]
fn resolve_edge_scan_resolves_a_conventional_child_of_a_path_overridden_module() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("cmd")).unwrap();
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"my-app\"\nversion = \"0.1.0\"\n\n\
[[bin]]\nname = \"edge-capsule\"\npath = \"cmd/edge.rs\"\n",
)
.unwrap();
std::fs::write(
dir.path().join("cmd/edge.rs"),
r#"
#[path = "shared.rs"]
mod handlers;
fn main() {
edge_routes![crate::handlers::child::show];
}
"#,
)
.unwrap();
std::fs::write(dir.path().join("cmd/shared.rs"), "mod child;\n").unwrap();
std::fs::write(
dir.path().join("cmd/child.rs"),
"#[edge]\npub fn show() {}\n",
)
.unwrap();
let scan = resolve_edge_scan_with_extra_file(
dir.path(),
&[],
Some(&dir.path().join("cmd/edge.rs")),
);
assert_eq!(scan.functions.len(), 1, "{:?}", scan.functions);
assert_eq!(scan.functions[0].file, "cmd/child.rs");
assert_eq!(
scan.functions[0].module_path,
vec!["handlers".to_owned(), "child".to_owned()]
);
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
}
#[test]
fn cfg_false_match_with_macro_scrutinee_does_not_leak_its_body_as_a_separate_statement() {
let scan = scan_one_with_features(
r#"
#[edge]
pub fn show() {}
macro_rules! value {
() => { 1 };
}
fn wire() {
#[cfg(feature = "premium")]
match value!{} {
_ => edge_routes![crate::show],
}
}
"#,
&[],
);
assert!(
scan.registered_fns().is_empty(),
"{:?}",
scan.registered_fns()
);
assert_eq!(scan.unregistered().len(), 1, "{:?}", scan.unregistered());
assert_eq!(scan.unregistered()[0].name, "show");
}
#[test]
fn resolve_edge_scan_keeps_the_librarys_own_identity_for_a_file_the_capsule_also_touches() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("src/bin")).unwrap();
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"my-app\"\nversion = \"0.1.0\"\n",
)
.unwrap();
std::fs::write(
dir.path().join("src/lib.rs"),
"mod handlers;\nfn wire() { edge_routes![my_app::handlers::show]; }\n",
)
.unwrap();
std::fs::write(
dir.path().join("src/handlers.rs"),
"#[edge]\npub fn show() {}\n",
)
.unwrap();
std::fs::write(
dir.path().join("src/bin/edge-capsule.rs"),
r#"
#[path = "../handlers.rs"]
mod local_handlers;
fn main() { edge_routes![crate::local_handlers::show]; }
"#,
)
.unwrap();
let scan = resolve_edge_scan_with_extra_file(
dir.path(),
&[],
Some(&dir.path().join("src/bin/edge-capsule.rs")),
);
assert_eq!(scan.functions.len(), 2, "{:?}", scan.functions);
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
let identities: BTreeSet<(String, Vec<String>)> = scan
.functions
.iter()
.map(|f| (f.crate_root.clone(), f.module_path.clone()))
.collect();
assert!(
identities.contains(&(String::new(), vec!["handlers".to_owned()])),
"{identities:?}"
);
assert!(
identities.contains(&(
"bin:edge-capsule".to_owned(),
vec!["local_handlers".to_owned()]
)),
"{identities:?}"
);
}
#[test]
fn cfg_false_let_with_generic_type_argument_comma_does_not_leak_its_initializer() {
let scan = scan_one_with_features(
r#"
#[edge]
pub fn show() {}
fn wire() {
#[cfg(feature = "premium")]
let _: Result<(), ()> = {
edge_routes![crate::show];
Ok(())
};
}
"#,
&[],
);
assert!(
scan.registered_fns().is_empty(),
"{:?}",
scan.registered_fns()
);
assert_eq!(scan.unregistered().len(), 1, "{:?}", scan.unregistered());
assert_eq!(scan.unregistered()[0].name, "show");
}
#[test]
fn cfg_false_let_with_unmatched_comparison_operator_does_not_swallow_the_next_statement() {
let scan = scan_one_with_features(
r#"
#[edge]
pub fn show() {}
fn wire() {
#[cfg(feature = "premium")]
let disabled = 1 < 2;
edge_routes![crate::show];
}
"#,
&[],
);
assert_eq!(
scan.registered_fns().len(),
1,
"{:?}",
scan.registered_fns()
);
assert_eq!(scan.registered_fns()[0].name, "show");
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
}
#[test]
fn cfg_false_match_arm_with_unmatched_comparison_operator_does_not_swallow_the_next_arm() {
let scan = scan_one_with_features(
r#"
#[edge]
pub fn show() {}
fn wire(x: i32) {
match x {
#[cfg(feature = "premium")]
0 => 1 < 2,
_ => edge_routes![crate::show],
};
}
"#,
&[],
);
assert_eq!(
scan.registered_fns().len(),
1,
"{:?}",
scan.registered_fns()
);
assert_eq!(scan.registered_fns()[0].name, "show");
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
}
#[test]
fn cfg_false_match_arm_with_unmatched_comparison_operator_does_not_pair_across_the_next_arm() {
let scan = scan_one_with_features(
r#"
#[edge]
pub fn show() -> Vec<i32> { vec![] }
fn wire(x: i32) {
match x {
#[cfg(feature = "premium")]
0 => 1 < 2,
_ => edge_routes![crate::show].len() > 0,
};
}
"#,
&[],
);
assert_eq!(
scan.registered_fns().len(),
1,
"{:?}",
scan.registered_fns()
);
assert_eq!(scan.registered_fns()[0].name, "show");
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
}
#[test]
fn cfg_false_if_with_async_block_condition_does_not_leak_its_body_as_a_separate_statement() {
let scan = scan_one_with_features(
r#"
#[edge]
pub fn show() {}
async fn predicate() -> bool { true }
async fn wire() {
#[cfg(feature = "premium")]
if async { predicate().await }.await {
edge_routes![crate::show];
}
}
"#,
&[],
);
assert!(
scan.registered_fns().is_empty(),
"{:?}",
scan.registered_fns()
);
assert_eq!(scan.unregistered().len(), 1, "{:?}", scan.unregistered());
assert_eq!(scan.unregistered()[0].name, "show");
}
#[test]
fn cfg_false_if_with_async_move_block_condition_does_not_leak_its_body_as_a_separate_statement()
{
let scan = scan_one_with_features(
r#"
#[edge]
pub fn show() {}
async fn wire() {
#[cfg(feature = "premium")]
if async move { true }.await {
edge_routes![crate::show];
}
}
"#,
&[],
);
assert!(
scan.registered_fns().is_empty(),
"{:?}",
scan.registered_fns()
);
assert_eq!(scan.unregistered().len(), 1, "{:?}", scan.unregistered());
assert_eq!(scan.unregistered()[0].name, "show");
}
#[test]
fn cfg_false_if_with_bare_block_condition_does_not_leak_its_body_as_a_separate_statement() {
let scan = scan_one_with_features(
r#"
#[edge]
pub fn show() {}
fn wire() {
#[cfg(feature = "premium")]
if { true } {
edge_routes![crate::show];
}
}
"#,
&[],
);
assert!(
scan.registered_fns().is_empty(),
"{:?}",
scan.registered_fns()
);
assert_eq!(scan.unregistered().len(), 1, "{:?}", scan.unregistered());
assert_eq!(scan.unregistered()[0].name, "show");
}
#[test]
fn cfg_false_if_with_inline_const_block_condition_does_not_leak_its_body_as_a_separate_statement()
{
let scan = scan_one_with_features(
r#"
#[edge]
pub fn show() {}
fn wire() {
#[cfg(feature = "premium")]
if const { true } {
edge_routes![crate::show];
}
}
"#,
&[],
);
assert!(
scan.registered_fns().is_empty(),
"{:?}",
scan.registered_fns()
);
assert_eq!(scan.unregistered().len(), 1, "{:?}", scan.unregistered());
assert_eq!(scan.unregistered()[0].name, "show");
}
#[test]
fn cfg_false_if_with_unsafe_block_condition_does_not_leak_its_body_as_a_separate_statement() {
let scan = scan_one_with_features(
r#"
#[edge]
pub fn show() {}
fn wire() {
#[cfg(feature = "premium")]
if unsafe { true } {
edge_routes![crate::show];
}
}
"#,
&[],
);
assert!(
scan.registered_fns().is_empty(),
"{:?}",
scan.registered_fns()
);
assert_eq!(scan.unregistered().len(), 1, "{:?}", scan.unregistered());
assert_eq!(scan.unregistered()[0].name, "show");
}
#[test]
fn resolve_edge_scan_keeps_the_conventional_identity_beside_a_path_alias() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("src")).unwrap();
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"my-app\"\nversion = \"0.1.0\"\n",
)
.unwrap();
std::fs::write(
dir.path().join("src/lib.rs"),
r#"
mod handlers;
#[path = "handlers.rs"]
mod alias;
fn wire() {
edge_routes![crate::handlers::show];
edge_routes![crate::alias::show];
}
"#,
)
.unwrap();
std::fs::write(
dir.path().join("src/handlers.rs"),
"#[edge]\npub fn show() {}\n",
)
.unwrap();
let scan = resolve_edge_scan(dir.path());
assert_eq!(scan.functions.len(), 2, "{:?}", scan.functions);
assert!(scan.unregistered().is_empty(), "{:?}", scan.unregistered());
let module_paths: BTreeSet<Vec<String>> = scan
.functions
.iter()
.map(|f| f.module_path.clone())
.collect();
assert!(
module_paths.contains(&vec!["handlers".to_owned()]),
"{module_paths:?}"
);
assert!(
module_paths.contains(&vec!["alias".to_owned()]),
"{module_paths:?}"
);
}
}