fixrs 0.1.0

Blazing-fast CLI to replace Rust qualified paths with use statements, auto-fixing clippy::absolute_paths
Documentation
use std::fmt::Write;

use syn::{File, Item};

use crate::hash::HashSet;

/// 记录当前文件顶层的作用域信息(已定义的符号与已存在的 use 语句)
#[derive(Debug, Default, Clone)]
pub struct FileScope {
  /// 顶层在作用域内的所有名称(函数名、类型名、mod名、use 导入的名等)
  pub in_scope_idents: HashSet<String>,
  /// 顶层所有显式 use 导入的完整路径(如 "std::io::Error", "std::io" 等)
  pub existing_use_paths: HashSet<String>,
}

/// 收集文件顶层定义的所有标识符与现有 use 导入路径
pub fn collect_file_scope(file: &File) -> FileScope {
  let mut scope = FileScope::default();
  let mut buf = String::with_capacity(64);

  for item in &file.items {
    let ident = match item {
      Item::Fn(f) => Some(&f.sig.ident),
      Item::Struct(s) => Some(&s.ident),
      Item::Enum(e) => Some(&e.ident),
      Item::Const(c) => Some(&c.ident),
      Item::Static(s) => Some(&s.ident),
      Item::Trait(t) => Some(&t.ident),
      Item::Type(t) => Some(&t.ident),
      Item::Mod(m) => Some(&m.ident),
      Item::ExternCrate(ec) => Some(ec.rename.as_ref().map_or(&ec.ident, |(_, r)| r)),
      Item::Use(u) => {
        buf.clear();
        walk_use_tree(&u.tree, &mut buf, &mut scope);
        None
      }
      _ => None,
    };
    if let Some(ident) = ident {
      scope.in_scope_idents.insert(ident.to_string());
    }
  }

  scope
}

fn walk_use_tree(tree: &syn::UseTree, prefix: &mut String, scope: &mut FileScope) {
  let prev_len = prefix.len();
  match tree {
    syn::UseTree::Path(p) => {
      if !prefix.is_empty() {
        prefix.push_str("::");
      }
      let _ = write!(prefix, "{}", p.ident);
      walk_use_tree(&p.tree, prefix, scope);
      prefix.truncate(prev_len);
    }
    syn::UseTree::Name(n) => {
      if n.ident == "self" {
        if !prefix.is_empty() {
          if let Some(tail) = prefix.rsplit("::").next() {
            scope.in_scope_idents.insert(tail.to_string());
          }
          scope.existing_use_paths.insert(prefix.clone());
        }
      } else {
        let ident = n.ident.to_string();
        let full_path = if prefix.is_empty() {
          ident.clone()
        } else {
          format!("{prefix}::{ident}")
        };
        scope.in_scope_idents.insert(ident);
        scope.existing_use_paths.insert(full_path);
      }
    }
    syn::UseTree::Rename(r) => {
      if r.rename != "_" {
        scope.in_scope_idents.insert(r.rename.to_string());
      }
      // 只有未重命名且非匿名引入时,原路径才算作以原名在作用域内
      if r.ident != "_" && r.rename == r.ident {
        let full_path = if r.ident == "self" {
          prefix.clone()
        } else if prefix.is_empty() {
          r.ident.to_string()
        } else {
          format!("{prefix}::{}", r.ident)
        };
        scope.existing_use_paths.insert(full_path);
      }
    }
    syn::UseTree::Group(g) => {
      for item in &g.items {
        walk_use_tree(item, prefix, scope);
      }
    }
    syn::UseTree::Glob(_) => {
      if !prefix.is_empty() {
        scope.existing_use_paths.insert(format!("{prefix}::*"));
      }
    }
  }
}