use crate::*;
pub(crate) fn path_to_string(p: &syn::Path) -> String {
p.segments.iter().map(|s| s.ident.to_string()).collect::<Vec<_>>().join("::")
}
pub(crate) fn bound_leaves(bounds: &syn::punctuated::Punctuated<syn::TypeParamBound, syn::Token![+]>) -> Vec<String> {
bounds
.iter()
.filter_map(|b| match b {
syn::TypeParamBound::Trait(t) => t.path.segments.last().map(|s| s.ident.to_string()),
_ => None,
})
.collect()
}
pub(crate) fn trait_leaves(ty: &syn::Type, generic_bounds: &HashMap<String, Vec<String>>) -> Vec<String> {
match ty {
syn::Type::Reference(r) => trait_leaves(&r.elem, generic_bounds),
syn::Type::Paren(p) => trait_leaves(&p.elem, generic_bounds),
syn::Type::Group(g) => trait_leaves(&g.elem, generic_bounds),
syn::Type::TraitObject(t) => bound_leaves(&t.bounds),
syn::Type::ImplTrait(t) => bound_leaves(&t.bounds),
syn::Type::Path(p) => {
if let Some(id) = p.path.get_ident() {
return generic_bounds.get(&id.to_string()).cloned().unwrap_or_default();
}
let Some(seg) = p.path.segments.last() else { return Vec::new() };
let wrapper = matches!(seg.ident.to_string().as_str(), "Box" | "Rc" | "Arc" | "RefCell" | "Mutex" | "RwLock" | "Cell");
if !wrapper {
return Vec::new();
}
let syn::PathArguments::AngleBracketed(args) = &seg.arguments else { return Vec::new() };
args.args
.iter()
.find_map(|a| match a {
syn::GenericArgument::Type(inner) => Some(trait_leaves(inner, generic_bounds)),
_ => None,
})
.unwrap_or_default()
}
_ => Vec::new(),
}
}
pub(crate) fn is_callable_type(ty: &syn::Type, generic_bounds: &HashMap<String, Vec<String>>) -> bool {
match ty {
syn::Type::BareFn(_) => true,
syn::Type::Reference(r) => is_callable_type(&r.elem, generic_bounds),
syn::Type::Paren(p) => is_callable_type(&p.elem, generic_bounds),
syn::Type::Group(g) => is_callable_type(&g.elem, generic_bounds),
_ => trait_leaves(ty, generic_bounds)
.iter()
.any(|l| matches!(l.as_str(), "Fn" | "FnMut" | "FnOnce")),
}
}
pub(crate) fn block_tail_expr(b: &syn::Block) -> Option<&syn::Expr> {
match b.stmts.last() {
Some(syn::Stmt::Expr(e, None)) => Some(e),
_ => None,
}
}
pub(crate) fn seed_fn_typed_vars(sig: &syn::Signature) -> std::collections::HashSet<String> {
let gb = generic_bounds_of(sig);
let mut s = std::collections::HashSet::new();
for arg in &sig.inputs {
if let syn::FnArg::Typed(pt) = arg {
if let syn::Pat::Ident(id) = &*pt.pat {
if is_callable_type(&pt.ty, &gb) {
s.insert(id.ident.to_string());
}
}
}
}
s
}
pub(crate) fn generic_bounds_of(sig: &syn::Signature) -> HashMap<String, Vec<String>> {
generic_bounds_of_generics(&sig.generics)
}
pub(crate) fn generic_bounds_of_generics(generics: &syn::Generics) -> HashMap<String, Vec<String>> {
let mut m: HashMap<String, Vec<String>> = HashMap::new();
for gp in &generics.params {
if let syn::GenericParam::Type(tp) = gp {
let leaves = bound_leaves(&tp.bounds);
if !leaves.is_empty() {
m.entry(tp.ident.to_string()).or_default().extend(leaves);
}
}
}
if let Some(w) = &generics.where_clause {
for pred in &w.predicates {
if let syn::WherePredicate::Type(pt) = pred {
if let syn::Type::Path(p) = &pt.bounded_ty {
if let Some(id) = p.path.get_ident() {
let leaves = bound_leaves(&pt.bounds);
if !leaves.is_empty() {
m.entry(id.to_string()).or_default().extend(leaves);
}
}
}
}
}
}
m
}
pub(crate) fn type_path(ty: &syn::Type, uses: &HashMap<String, String>) -> Option<String> {
match ty {
syn::Type::Reference(r) => type_path(&r.elem, uses),
syn::Type::Paren(p) => type_path(&p.elem, uses),
syn::Type::Group(g) => type_path(&g.elem, uses),
syn::Type::Path(p) => {
if let Some(seg) = p.path.segments.last() {
if matches!(seg.ident.to_string().as_str(), "Box" | "Arc" | "Rc") {
if let syn::PathArguments::AngleBracketed(args) = &seg.arguments {
if let Some(inner) = args.args.iter().find_map(|a| match a {
syn::GenericArgument::Type(t) => Some(t),
_ => None,
}) {
return type_path(inner, uses);
}
}
}
}
Some(expand(&path_to_string(&p.path), uses))
}
_ => None,
}
}
pub(crate) fn elem_type(ty: &syn::Type, uses: &HashMap<String, String>) -> Option<String> {
match ty {
syn::Type::Reference(r) => elem_type(&r.elem, uses),
syn::Type::Paren(p) => elem_type(&p.elem, uses),
syn::Type::Group(g) => elem_type(&g.elem, uses),
syn::Type::Slice(s) => type_path(&s.elem, uses),
syn::Type::Array(a) => type_path(&a.elem, uses),
syn::Type::Path(p) => {
let seg = p.path.segments.last()?;
let name = seg.ident.to_string();
let syn::PathArguments::AngleBracketed(args) = &seg.arguments else { return None };
let first_ty = args.args.iter().find_map(|a| match a {
syn::GenericArgument::Type(t) => Some(t),
_ => None,
})?;
match name.as_str() {
"Vec" | "VecDeque" | "HashSet" | "BTreeSet" | "ContiguousArray" | "BinaryHeap"
| "LinkedList" => type_path(first_ty, uses),
"Box" | "Arc" | "Rc" => elem_type(first_ty, uses),
_ => None,
}
}
_ => None,
}
}
pub(crate) fn tuple_types(ty: &syn::Type, uses: &HashMap<String, String>) -> Option<Vec<Option<String>>> {
match ty {
syn::Type::Reference(r) => tuple_types(&r.elem, uses),
syn::Type::Paren(p) => tuple_types(&p.elem, uses),
syn::Type::Group(g) => tuple_types(&g.elem, uses),
syn::Type::Tuple(t) if t.elems.len() >= 2 => {
Some(t.elems.iter().map(|e| type_path(e, uses)).collect())
}
_ => None,
}
}
pub(crate) fn is_ctor(name: &str) -> bool {
matches!(
name,
"new" | "default" | "builder" | "with_capacity" | "connect" | "open" | "init" | "from"
| "from_path" | "from_str" | "with_config" | "create"
)
}
pub(crate) fn ctor_type(expr: &syn::Expr, uses: &HashMap<String, String>, returns: &ReturnIndex) -> Option<String> {
match expr {
syn::Expr::Reference(r) => ctor_type(&r.expr, uses, returns),
syn::Expr::Paren(p) => ctor_type(&p.expr, uses, returns),
syn::Expr::Try(t) => ctor_type(&t.expr, uses, returns),
syn::Expr::Await(a) => ctor_type(&a.base, uses, returns),
syn::Expr::Call(c) => {
let syn::Expr::Path(p) = &*c.func else { return None };
let full = path_to_string(&p.path);
let leaf = full.rsplit("::").next().unwrap_or(&full);
if let Some((ty, last)) = full.rsplit_once("::") {
let ty_leaf = ty.rsplit("::").next().unwrap_or(ty);
let type_like = ty_leaf.chars().next().is_some_and(|c| c.is_uppercase());
if is_ctor(last) && type_like {
return Some(expand(ty, uses));
}
}
returns.get(leaf).filter(|t| *t != RET_FN_TYPED).cloned()
}
syn::Expr::Struct(s) => type_from_value_path(&path_to_string(&s.path), uses),
syn::Expr::Path(p) => type_from_value_path(&path_to_string(&p.path), uses),
_ => None,
}
}
pub(crate) fn type_from_value_path(full: &str, uses: &HashMap<String, String>) -> Option<String> {
let camel = |s: &str| {
let mut ch = s.chars();
ch.next().is_some_and(|c| c.is_uppercase())
&& (s.chars().count() == 1 || s.chars().any(|c| c.is_lowercase()))
};
let segs: Vec<&str> = full.split("::").collect();
let last = segs.last()?;
if !camel(last) {
return None;
}
if segs.len() >= 2 && camel(segs[segs.len() - 2]) {
return Some(expand(&segs[..segs.len() - 1].join("::"), uses));
}
Some(expand(full, uses))
}
pub(crate) fn unwrap_result_option(ty: &syn::Type) -> &syn::Type {
let syn::Type::Path(p) = ty else { return ty };
let Some(seg) = p.path.segments.last() else { return ty };
if matches!(seg.ident.to_string().as_str(), "Result" | "Option" | "IoResult") {
if let syn::PathArguments::AngleBracketed(args) = &seg.arguments {
if let Some(syn::GenericArgument::Type(inner)) = args.args.first() {
return inner;
}
}
}
ty
}
pub(crate) fn result_err_leaf(output: &syn::ReturnType, uses: &HashMap<String, String>) -> Option<String> {
let syn::ReturnType::Type(_, ty) = output else { return None };
let syn::Type::Path(p) = &**ty else { return None };
let seg = p.path.segments.last()?;
if seg.ident != "Result" {
return None; }
let syn::PathArguments::AngleBracketed(args) = &seg.arguments else { return None };
let mut tys = args.args.iter().filter_map(|a| match a {
syn::GenericArgument::Type(t) => Some(t),
_ => None,
});
let _ok = tys.next()?;
let err = tys.next()?;
let expanded = type_path(err, uses)?;
Some(expanded.rsplit("::").next().unwrap_or(&expanded).to_string())
}
pub(crate) fn expand(path: &str, uses: &HashMap<String, String>) -> String {
let mut segs: Vec<&str> = path.split("::").collect();
let rooted_local = matches!(segs.first().copied(), Some("crate" | "self" | "super"));
while matches!(segs.first().copied(), Some("crate" | "self" | "super")) {
segs.remove(0);
}
if segs.is_empty() {
return path.to_string();
}
if !rooted_local {
if let Some(full) = uses.get(segs[0]) {
let rest = &segs[1..];
return if rest.is_empty() { full.clone() } else { format!("{full}::{}", rest.join("::")) };
}
}
segs.join("::")
}
pub(crate) fn first_str_lit(args: &syn::punctuated::Punctuated<syn::Expr, syn::token::Comma>) -> Option<String> {
for a in args {
if let syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(s), .. }) = a {
let v = s.value();
if !v.trim().is_empty() {
return Some(v);
}
}
}
None
}
pub(crate) fn single_pat_ident(pat: &syn::Pat) -> Option<String> {
match pat {
syn::Pat::Ident(id) => Some(id.ident.to_string()),
syn::Pat::Reference(r) => single_pat_ident(&r.pat),
syn::Pat::Paren(p) => single_pat_ident(&p.pat),
syn::Pat::Type(t) => single_pat_ident(&t.pat),
_ => None,
}
}
pub(crate) fn arm_payload_binding(pat: &syn::Pat, enum_variants: &EnumVariantIndex) -> Option<(String, Option<String>)> {
let ts = match pat {
syn::Pat::TupleStruct(ts) => ts,
syn::Pat::Reference(r) => return arm_payload_binding(&r.pat, enum_variants),
syn::Pat::Paren(p) => return arm_payload_binding(&p.pat, enum_variants),
_ => return None,
};
if ts.elems.len() != 1 {
return None; }
let name = single_pat_ident(ts.elems.first()?)?;
let variant_leaf = ts.path.segments.last()?.ident.to_string();
let ty = enum_variants.get(&variant_leaf).cloned();
Some((name, ty))
}
pub(crate) fn has_cfg(attrs: &[syn::Attribute]) -> bool {
attrs.iter().any(|a| a.path().is_ident("cfg"))
}
pub(crate) fn is_test_file_stem(stem: &str) -> bool {
stem == "tests" || stem == "test" || stem.ends_with("_tests") || stem.ends_with("_test")
}
pub(crate) fn is_build_script(rel: &std::path::Path) -> bool {
rel == std::path::Path::new("build.rs")
}
pub(crate) fn cfg_meta_requires_test(m: &syn::meta::ParseNestedMeta) -> bool {
if m.path.is_ident("test") {
return true;
}
if m.path.is_ident("any") || m.path.is_ident("all") {
let mut inner_test = false;
let _ = m.parse_nested_meta(|inner| {
if cfg_meta_requires_test(&inner) {
inner_test = true;
}
Ok(())
});
return inner_test;
}
false }
pub(crate) fn is_cfg_test(attrs: &[syn::Attribute]) -> bool {
attrs.iter().any(|a| {
a.path().is_ident("cfg") && {
let mut found = false;
let _ = a.parse_nested_meta(|m| {
if cfg_meta_requires_test(&m) {
found = true;
}
Ok(())
});
found
}
})
}
pub(crate) type FeatureSets = (std::collections::HashSet<String>, std::collections::HashSet<String>);
pub(crate) static CFG_FEATURES: std::sync::OnceLock<std::sync::RwLock<FeatureSets>> = std::sync::OnceLock::new();
pub(crate) fn cfg_cell() -> &'static std::sync::RwLock<FeatureSets> {
CFG_FEATURES.get_or_init(|| std::sync::RwLock::new((Default::default(), Default::default())))
}
pub(crate) fn set_cfg_features(f: FeatureSets) {
*cfg_cell().write().unwrap() = f;
}
pub(crate) fn active_features_sorted() -> Vec<String> {
let mut v: Vec<String> = cfg_cell().read().unwrap().0.iter().cloned().collect();
v.sort();
v
}
pub(crate) fn push_quoted(s: &str, out: &mut Vec<String>) {
let mut rest = s;
while let Some(i) = rest.find('"') {
rest = &rest[i + 1..];
if let Some(j) = rest.find('"') {
out.push(rest[..j].to_string());
rest = &rest[j + 1..];
} else {
break;
}
}
}
pub(crate) fn parse_features(root: &std::path::Path) -> (std::collections::HashSet<String>, std::collections::HashSet<String>) {
use std::collections::{HashMap, HashSet};
let txt = match std::fs::read_to_string(root.join("Cargo.toml")) {
Ok(t) => t,
Err(_) => return (HashSet::new(), HashSet::new()),
};
let mut feats: HashMap<String, Vec<String>> = HashMap::new();
let mut in_features = false;
let mut cur: Option<(String, Vec<String>)> = None; for line in txt.lines() {
if let Some((k, vals)) = cur.as_mut() {
push_quoted(line, vals);
if line.contains(']') {
feats.insert(std::mem::take(k), std::mem::take(vals));
cur = None;
}
continue;
}
let t = line.trim();
if let Some(sec) = toml_section(line) {
in_features = sec == "features";
continue;
}
if !in_features || t.is_empty() || t.starts_with('#') {
continue;
}
if let Some(eq) = t.find('=') {
let key = t[..eq].trim().trim_matches('"').to_string();
let rhs = t[eq + 1..].trim();
if let Some(arr) = rhs.strip_prefix('[') {
let mut vals = Vec::new();
push_quoted(arr, &mut vals);
if rhs.contains(']') {
feats.insert(key, vals); } else {
cur = Some((key, vals)); }
}
}
}
let declared: HashSet<String> = feats.keys().cloned().collect();
let mut active: HashSet<String> = HashSet::new();
let mut stack: Vec<String> = feats.get("default").cloned().unwrap_or_default();
while let Some(f) = stack.pop() {
if f.contains(':') || f.contains('/') {
continue;
}
if active.insert(f.clone()) {
if let Some(next) = feats.get(&f) {
stack.extend(next.iter().cloned());
}
}
}
(active, declared)
}
pub(crate) fn cfg_eval(m: &syn::meta::ParseNestedMeta, active: &std::collections::HashSet<String>,
declared: &std::collections::HashSet<String>) -> Option<bool> {
if m.path.is_ident("feature") {
let v = m.value().ok().and_then(|v| v.parse::<syn::LitStr>().ok());
return v.and_then(|lit| {
let name = lit.value();
if active.contains(&name) {
Some(true)
} else if declared.contains(&name) {
Some(false)
} else {
None
}
});
}
if m.path.is_ident("not") {
let mut inner: Option<bool> = None;
let _ = m.parse_nested_meta(|n| { inner = cfg_eval(&n, active, declared); Ok(()) });
return inner.map(|b| !b);
}
if m.path.is_ident("all") {
let (mut any_false, mut all_true, mut saw) = (false, true, false);
let _ = m.parse_nested_meta(|n| { saw = true; match cfg_eval(&n, active, declared) { Some(false) => any_false = true, Some(true) => {}, None => all_true = false }; Ok(()) });
if any_false { return Some(false); }
if saw && all_true { return Some(true); }
return None;
}
if m.path.is_ident("any") {
let (mut any_true, mut all_false, mut saw) = (false, true, false);
let _ = m.parse_nested_meta(|n| { saw = true; match cfg_eval(&n, active, declared) { Some(true) => any_true = true, Some(false) => {}, None => all_false = false }; Ok(()) });
if any_true { return Some(true); }
if saw && all_false { return Some(false); }
return None;
}
None }
pub(crate) fn is_cfg_inactive(attrs: &[syn::Attribute]) -> bool {
if !attrs.iter().any(|a| a.path().is_ident("cfg")) {
return false; }
let guard = cfg_cell().read().unwrap();
let (active, declared) = &*guard;
if declared.is_empty() {
return false; }
attrs.iter().any(|a| {
a.path().is_ident("cfg") && {
let mut verdict: Option<bool> = None;
let _ = a.parse_nested_meta(|m| { verdict = cfg_eval(&m, active, declared); Ok(()) });
verdict == Some(false)
}
})
}
pub(crate) fn expr_attrs(e: &syn::Expr) -> &[syn::Attribute] {
match e {
syn::Expr::Block(x) => &x.attrs,
syn::Expr::If(x) => &x.attrs,
syn::Expr::Match(x) => &x.attrs,
syn::Expr::Unsafe(x) => &x.attrs,
syn::Expr::ForLoop(x) => &x.attrs,
syn::Expr::While(x) => &x.attrs,
syn::Expr::Loop(x) => &x.attrs,
syn::Expr::Call(x) => &x.attrs,
syn::Expr::MethodCall(x) => &x.attrs,
syn::Expr::Macro(x) => &x.attrs,
syn::Expr::Async(x) => &x.attrs,
syn::Expr::Const(x) => &x.attrs,
_ => &[],
}
}
pub(crate) fn stmt_cfg_inactive(stmt: &syn::Stmt) -> bool {
match stmt {
syn::Stmt::Local(l) => is_cfg_inactive(&l.attrs),
syn::Stmt::Macro(m) => is_cfg_inactive(&m.attrs),
syn::Stmt::Expr(e, _) => is_cfg_inactive(expr_attrs(e)),
syn::Stmt::Item(_) => false, }
}
pub(crate) fn impl_type_name(ty: &syn::Type) -> Option<String> {
if let syn::Type::Path(p) = ty {
return p.path.segments.last().map(|s| s.ident.to_string());
}
None
}
pub(crate) fn is_non_nominal_type(ty: &syn::Type) -> bool {
match ty {
syn::Type::Array(_) | syn::Type::Slice(_) | syn::Type::Tuple(_)
| syn::Type::Ptr(_) | syn::Type::Reference(_) | syn::Type::BareFn(_) => true,
syn::Type::Path(p) if p.qself.is_none() && p.path.segments.len() == 1 => {
const PRIMS: &[&str] = &[
"u8", "u16", "u32", "u64", "u128", "usize", "i8", "i16", "i32", "i64", "i128",
"isize", "f32", "f64", "bool", "char", "str",
];
let seg = &p.path.segments[0];
matches!(seg.arguments, syn::PathArguments::None)
&& PRIMS.contains(&seg.ident.to_string().as_str())
}
_ => false,
}
}
pub(crate) fn collect_use(tree: &syn::UseTree, prefix: String, out: &mut HashMap<String, String>) {
let join = |p: &str, s: &str| if p.is_empty() { s.to_string() } else { format!("{p}::{s}") };
match tree {
syn::UseTree::Path(p) => collect_use(&p.tree, join(&prefix, &p.ident.to_string()), out),
syn::UseTree::Name(n) => {
let id = n.ident.to_string();
if id == "self" {
if let Some(last) = prefix.rsplit("::").next() {
out.insert(last.to_string(), prefix.clone());
}
} else {
out.insert(id.clone(), join(&prefix, &id));
}
}
syn::UseTree::Rename(r) => {
out.insert(r.rename.to_string(), join(&prefix, &r.ident.to_string()));
}
syn::UseTree::Group(g) => {
for t in &g.items {
collect_use(t, prefix.clone(), out);
}
}
syn::UseTree::Glob(_) => {}
}
}
pub(crate) fn module_path(rel: &Path) -> String {
let mut comps: Vec<String> =
rel.components().filter_map(|c| c.as_os_str().to_str().map(String::from)).collect();
if let Some(i) = comps.iter().rposition(|c| c == "src") {
comps.drain(..=i);
}
if let Some(last) = comps.last() {
let stem = last.trim_end_matches(".rs").to_string();
if stem == "lib" || stem == "main" || stem == "mod" {
comps.pop();
} else {
let parts: Vec<String> = stem.split('.').map(String::from).collect();
comps.pop();
comps.extend(parts);
}
}
comps.join("::")
}
pub(crate) fn tail2(path: &str) -> Option<String> {
let segs: Vec<&str> = path.split("::").collect();
let n = segs.len();
if n < 2 {
return None;
}
Some(format!("{}::{}", segs[n - 2], segs[n - 1]))
}
pub(crate) fn is_iter_consumer(leaf: &str) -> bool {
matches!(
leaf,
"collect"
| "count"
| "sum"
| "product"
| "for_each"
| "try_for_each"
| "last"
| "nth"
| "fold"
| "try_fold"
| "reduce"
| "min"
| "max"
| "min_by"
| "max_by"
| "min_by_key"
| "max_by_key"
| "all"
| "any"
| "find"
| "find_map"
| "position"
| "rposition"
| "partition"
| "unzip"
| "collect_into"
)
}
pub(crate) fn is_format_macro(leaf: &str) -> bool {
matches!(
leaf,
"format"
| "format_args"
| "print"
| "println"
| "eprint"
| "eprintln"
| "write"
| "writeln"
| "panic"
| "unreachable"
| "todo"
| "unimplemented"
| "assert"
| "assert_eq"
| "assert_ne"
| "debug_assert"
| "debug_assert_eq"
| "debug_assert_ne"
)
}
pub(crate) fn binop_trait(op: &syn::BinOp) -> Option<(&'static str, &'static str)> {
use syn::BinOp;
Some(match op {
BinOp::Add(_) => ("Add", "add"),
BinOp::Sub(_) => ("Sub", "sub"),
BinOp::Mul(_) => ("Mul", "mul"),
BinOp::Div(_) => ("Div", "div"),
BinOp::Rem(_) => ("Rem", "rem"),
BinOp::BitAnd(_) => ("BitAnd", "bitand"),
BinOp::BitOr(_) => ("BitOr", "bitor"),
BinOp::BitXor(_) => ("BitXor", "bitxor"),
BinOp::Shl(_) => ("Shl", "shl"),
BinOp::Shr(_) => ("Shr", "shr"),
BinOp::Eq(_) | BinOp::Ne(_) => ("PartialEq", "eq"),
BinOp::Lt(_) | BinOp::Le(_) | BinOp::Gt(_) | BinOp::Ge(_) => ("PartialOrd", "partial_cmp"),
_ => return None,
})
}
pub(crate) fn expr_is_into_call(expr: &syn::Expr) -> bool {
match expr {
syn::Expr::MethodCall(m) => m.method == "into" && m.args.is_empty(),
syn::Expr::Reference(r) => expr_is_into_call(&r.expr),
syn::Expr::Paren(p) => expr_is_into_call(&p.expr),
syn::Expr::Group(g) => expr_is_into_call(&g.expr),
_ => false,
}
}
pub(crate) enum FmtArg {
Implicit,
Index(usize),
Named,
}
pub(crate) struct FmtHole {
pub(crate) arg: FmtArg,
pub(crate) debug: bool,
}
pub(crate) fn parse_format_holes(fmt: &str) -> Vec<FmtHole> {
let mut holes = Vec::new();
let bytes: Vec<char> = fmt.chars().collect();
let mut i = 0;
while i < bytes.len() {
match bytes[i] {
'{' => {
if bytes.get(i + 1) == Some(&'{') {
i += 2; continue;
}
let start = i + 1;
let mut j = start;
while j < bytes.len() && bytes[j] != '}' {
j += 1;
}
if j >= bytes.len() {
break; }
let inner: String = bytes[start..j].iter().collect();
let (name_part, spec) = match inner.split_once(':') {
Some((n, s)) => (n.trim(), s),
None => (inner.trim(), ""),
};
let debug = spec.trim_end().ends_with('?');
let arg = if name_part.is_empty() {
FmtArg::Implicit
} else if let Ok(idx) = name_part.parse::<usize>() {
FmtArg::Index(idx)
} else {
FmtArg::Named
};
holes.push(FmtHole { arg, debug });
i = j + 1;
}
'}' => {
i += if bytes.get(i + 1) == Some(&'}') { 2 } else { 1 }; }
_ => i += 1,
}
}
holes
}