use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use crate::read_census::{Tok, lex, strip_test_items};
#[derive(Clone, Debug)]
pub(crate) struct Source {
pub(crate) path: String,
pub(crate) krate: String,
pub(crate) text: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum Shape {
Concrete,
Forwarding,
Blanket { bounds: Vec<String> },
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) struct Implementor {
pub(crate) krate: String,
pub(crate) ident: String,
pub(crate) path: String,
}
#[derive(Clone, Debug, Default)]
pub(crate) struct Implementors {
pub(crate) concrete: BTreeSet<Implementor>,
pub(crate) forwarding: BTreeSet<Implementor>,
}
impl Implementors {
pub(crate) fn of(sources: &[Source], trait_name: &str) -> Self {
let mut out = Self::default();
let mut visited = BTreeSet::new();
out.collect(sources, trait_name, &mut visited);
out
}
fn collect(&mut self, sources: &[Source], trait_name: &str, visited: &mut BTreeSet<String>) {
if !visited.insert(trait_name.to_owned()) {
return;
}
for source in sources {
for (ident, shape) in impls_in(&source.text, trait_name) {
let found = Implementor {
krate: source.krate.clone(),
ident,
path: source.path.clone(),
};
match shape {
Shape::Concrete => {
self.concrete.insert(found);
}
Shape::Forwarding => {
self.forwarding.insert(found);
}
Shape::Blanket { bounds } => {
let [bound] = bounds.as_slice() else {
panic!(
"{}: a blanket impl of {trait_name} over {bounds:?} — the census \
follows exactly one inline bound; teach it this one",
source.path
);
};
self.collect(sources, bound, visited);
}
}
}
}
}
}
pub(crate) fn workspace_sources(root: &Path) -> Vec<Source> {
let mut sources = Vec::new();
for dir in sorted_entries(root) {
let name = dir
.file_name()
.and_then(|n| n.to_str())
.unwrap_or_default()
.to_owned();
if name.starts_with('.') || name == "target" {
continue;
}
let src = dir.join("src");
if !dir.join("Cargo.toml").is_file() || !src.is_dir() {
continue;
}
let krate = name.replace('-', "_");
for file in rust_files(&src) {
let text = std::fs::read_to_string(&file)
.unwrap_or_else(|e| panic!("reading {}: {e}", file.display()));
let path = file
.strip_prefix(root)
.unwrap_or(&file)
.display()
.to_string();
sources.push(Source {
path,
krate: krate.clone(),
text,
});
}
}
sources
}
fn sorted_entries(dir: &Path) -> Vec<PathBuf> {
let mut entries: Vec<PathBuf> = std::fs::read_dir(dir)
.unwrap_or_else(|e| panic!("listing {}: {e}", dir.display()))
.map(|entry| {
entry
.unwrap_or_else(|e| panic!("listing {}: {e}", dir.display()))
.path()
})
.collect();
entries.sort();
entries
}
fn rust_files(dir: &Path) -> Vec<PathBuf> {
let mut files = Vec::new();
for entry in sorted_entries(dir) {
if entry.is_dir() {
files.extend(rust_files(&entry));
} else if entry.extension().is_some_and(|e| e == "rs") {
files.push(entry);
}
}
files
}
const OURS: &[&str] = &["crate", "self", "super", "engenho_controllers"];
pub(crate) fn impls_in(src: &str, trait_name: &str) -> Vec<(String, Shape)> {
let tokens = strip_test_items(&lex(src));
let mut found = Vec::new();
for (i, tok) in tokens.iter().enumerate() {
if *tok == Tok::Ident("impl".into())
&& let Some(item) = trait_impl_at(&tokens, i + 1)
{
let ours = item.trait_path.split_last().is_some_and(|(last, prefix)| {
last == trait_name
&& prefix
.first()
.is_none_or(|first| OURS.contains(&first.as_str()))
});
if ours {
found.push((item.self_ident.clone(), item.shape(trait_name)));
}
}
}
found
}
#[derive(Debug)]
struct Param {
name: String,
bounds: Vec<String>,
}
#[derive(Debug)]
struct TraitImpl {
params: Vec<Param>,
trait_path: Vec<String>,
self_path: Vec<String>,
self_ident: String,
self_args: Vec<String>,
}
impl TraitImpl {
fn shape(&self, trait_name: &str) -> Shape {
let param = |name: &str| self.params.iter().find(|p| p.name == name);
if let [only] = self.self_path.as_slice()
&& let Some(p) = param(only)
{
return Shape::Blanket {
bounds: p
.bounds
.iter()
.filter(|b| !AUTO.contains(&b.as_str()))
.cloned()
.collect(),
};
}
let forwards = self
.self_args
.iter()
.filter_map(|a| param(a))
.any(|p| p.bounds.iter().any(|b| b == trait_name));
if forwards {
Shape::Forwarding
} else {
Shape::Concrete
}
}
}
const AUTO: &[&str] = &["Send", "Sync", "Sized", "Unpin"];
fn trait_impl_at(tokens: &[Tok], mut i: usize) -> Option<TraitImpl> {
let mut params = Vec::new();
if tokens.get(i) == Some(&Tok::Punct('<')) {
let close = angle_close(tokens, i)?;
params = generic_params(&tokens[i + 1..close]);
i = close + 1;
}
let for_at = scan_depth0(tokens, i, |t| *t == Tok::Ident("for".into()))?;
let trait_path = path_segments(&tokens[i..for_at]);
let body = scan_depth0(tokens, for_at + 1, |t| {
*t == Tok::Punct('{') || *t == Tok::Ident("where".into())
})?;
let self_tokens = &tokens[for_at + 1..body];
let self_path = path_segments(self_tokens);
let self_ident = self_path.last()?.clone();
let self_args = self_tokens
.iter()
.skip_while(|t| **t != Tok::Punct('<'))
.filter_map(|t| match t {
Tok::Ident(name) => Some(name.clone()),
Tok::Str(_) | Tok::Punct(_) | Tok::Other => None,
})
.collect();
Some(TraitImpl {
params,
trait_path,
self_path,
self_ident,
self_args,
})
}
fn scan_depth0(tokens: &[Tok], start: usize, hit: impl Fn(&Tok) -> bool) -> Option<usize> {
let mut depth = 0usize;
let mut i = start;
while i < tokens.len() {
let tok = &tokens[i];
if depth == 0 && hit(tok) {
return Some(i);
}
match tok {
Tok::Punct('(' | '[') => depth += 1,
Tok::Punct(')' | ']') => depth = depth.checked_sub(1)?,
Tok::Punct('<') => i = angle_close(tokens, i)?,
Tok::Punct('{' | ';') if depth == 0 => return None,
Tok::Ident(word) if depth == 0 && word == "where" => return None,
Tok::Ident(_) | Tok::Str(_) | Tok::Punct(_) | Tok::Other => {}
}
i += 1;
}
None
}
fn angle_close(tokens: &[Tok], open: usize) -> Option<usize> {
let mut depth = 0usize;
for i in open..tokens.len() {
match &tokens[i] {
Tok::Punct('<') => depth += 1,
Tok::Punct('>') if i > 0 && tokens[i - 1] == Tok::Punct('-') => {}
Tok::Punct('>') => {
depth = depth.checked_sub(1)?;
if depth == 0 {
return Some(i);
}
}
Tok::Ident(_) | Tok::Str(_) | Tok::Punct(_) | Tok::Other => {}
}
}
None
}
fn path_segments(tokens: &[Tok]) -> Vec<String> {
let mut segments = Vec::new();
let mut depth = 0usize;
for (i, tok) in tokens.iter().enumerate() {
match tok {
Tok::Punct('<') => depth += 1,
Tok::Punct('>') if i > 0 && tokens[i - 1] == Tok::Punct('-') => {}
Tok::Punct('>') => depth = depth.saturating_sub(1),
Tok::Ident(name) if depth == 0 && name != "dyn" => segments.push(name.clone()),
Tok::Ident(_) | Tok::Str(_) | Tok::Punct(_) | Tok::Other => {}
}
}
segments
}
fn generic_params(tokens: &[Tok]) -> Vec<Param> {
let mut params = Vec::new();
for part in split_top_level_commas(tokens) {
let mut idents = part.iter().filter_map(|t| match t {
Tok::Ident(name) => Some(name.clone()),
Tok::Str(_) | Tok::Punct(_) | Tok::Other => None,
});
let Some(mut name) = idents.next() else {
continue; };
if name == "const" {
let Some(real) = idents.next() else { continue };
name = real;
}
let bounds = part
.iter()
.position(|t| *t == Tok::Punct(':'))
.map(|colon| path_segments_per_bound(&part[colon + 1..]))
.unwrap_or_default();
params.push(Param { name, bounds });
}
params
}
fn path_segments_per_bound(tokens: &[Tok]) -> Vec<String> {
tokens
.split(|t| *t == Tok::Punct('+'))
.filter_map(|bound| path_segments(bound).last().cloned())
.collect()
}
fn split_top_level_commas(tokens: &[Tok]) -> Vec<&[Tok]> {
let mut parts = Vec::new();
let mut depth = 0usize;
let mut start = 0;
for (i, tok) in tokens.iter().enumerate() {
match tok {
Tok::Punct('(' | '[' | '{' | '<') => depth += 1,
Tok::Punct('>') if i > 0 && tokens[i - 1] == Tok::Punct('-') => {}
Tok::Punct(')' | ']' | '}' | '>') => depth = depth.saturating_sub(1),
Tok::Punct(',') if depth == 0 => {
parts.push(&tokens[start..i]);
start = i + 1;
}
Tok::Ident(_) | Tok::Str(_) | Tok::Punct(_) | Tok::Other => {}
}
}
parts.push(&tokens[start..]);
parts
}
#[cfg(test)]
mod tests {
use super::*;
fn impls(src: &str) -> Vec<(String, Shape)> {
impls_in(src, "Controller")
}
fn concrete(names: &[&str]) -> Vec<(String, Shape)> {
names
.iter()
.map(|n| ((*n).to_owned(), Shape::Concrete))
.collect()
}
#[test]
fn it_sees_every_impl_shape_it_claims() {
let found = impls(
r"
#[async_trait]
impl Controller for Plain {
fn name(&self) -> &'static str { 'x'; }
}
impl engenho_controllers::Controller for Qualified {}
impl crate::controller::Controller for InCrate {}
impl<E: Clone + Send + Sync + 'static> Controller for Generic<E> {}
impl<F: Fn() -> u8> Controller for WithArrow<F> {}
impl<C: Controller + ?Sized> Controller for std::sync::Arc<C> {}
impl<C: crate::Controller> Controller for Wrap<C> where C: Send {}
impl<T: OwnedChildrenReconciler + Send> Controller for T {}
",
);
let mut want = concrete(&["Plain", "Qualified", "InCrate", "Generic", "WithArrow"]);
want.push(("Arc".into(), Shape::Forwarding));
want.push(("Wrap".into(), Shape::Forwarding));
want.push((
"T".into(),
Shape::Blanket {
bounds: vec!["OwnedChildrenReconciler".into()],
},
));
assert_eq!(found, want);
}
#[test]
fn it_does_not_see_what_is_not_an_implementor() {
let found = impls(
r##"
// impl Controller for InAComment {}
/* impl Controller for InABlock {} */
const DOC: &str = "impl Controller for InAString {}";
const RAW: &str = r#"impl Controller for InARawString {}"#;
impl pb::controller_server::Controller for Grpc {}
impl TargetController for Other {}
impl ControllerExt for Ext {}
impl Inherent { fn f() {} }
impl<T> Inherent2<T> { fn g(&self) { for x in y {} } }
fn all() -> impl Iterator<Item = u8> { for c in cs {} }
fn arg(x: impl Fn(), y: u8) where for<'a> X: Y { for z in w {} }
#[cfg(test)]
mod tests {
impl Controller for InATest {}
}
#[cfg(test)]
impl Controller for InATestImpl {}
impl Controller for AfterTheTests {}
"##,
);
assert_eq!(found, concrete(&["AfterTheTests"]));
}
#[test]
fn a_blanket_impl_is_followed_to_its_bounds_implementors() {
let source = |path: &str, text: &str| Source {
path: path.to_owned(),
krate: "fixture".to_owned(),
text: text.to_owned(),
};
let sources = [
source(
"owned.rs",
"impl<T: OwnedChildrenReconciler> Controller for T {}\n\
impl<C: Controller> Controller for Adapter<C> {}",
),
source(
"deployment.rs",
"impl OwnedChildrenReconciler for DeploymentController {}",
),
source("gc.rs", "impl Controller for GcController {}"),
];
let found = Implementors::of(&sources, "Controller");
let idents =
|set: &BTreeSet<Implementor>| set.iter().map(|i| i.ident.clone()).collect::<Vec<_>>();
assert_eq!(
idents(&found.concrete),
["DeploymentController", "GcController"]
);
assert_eq!(idents(&found.forwarding), ["Adapter"]);
}
#[test]
#[should_panic(expected = "teach it this one")]
fn a_blanket_impl_it_cannot_follow_stops_the_census() {
let sources = [Source {
path: "where.rs".to_owned(),
krate: "fixture".to_owned(),
text: "impl<T> Controller for T where T: Other {}".to_owned(),
}];
let _ = Implementors::of(&sources, "Controller");
}
}