fixrs 0.1.0

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

use crate::{
  hash::{HashMap, HashSet},
  options::Options,
  parser::{FileScope, QualifiedPath},
};

#[derive(Debug, Clone)]
pub struct RewriteResult {
  pub content: String,
  pub replacements: Vec<(String, String)>,
}

#[derive(Debug)]
struct ReplaceAction {
  start: usize,
  end: usize,
  original: String,
  replacement: String,
}

/// 构建每行的起始字节偏移索引(基于 memchr SIMD 极速向量化扫描,预分配容量)
pub fn compute_line_offsets(source: &str) -> Vec<usize> {
  let mut offsets = Vec::with_capacity(source.len() / 40 + 1);
  offsets.push(0);
  for idx in memchr::memchr_iter(b'\n', source.as_bytes()) {
    offsets.push(idx + 1);
  }
  offsets
}

/// 对单个文件的源代码执行绝对路径替换与 use 注入(单次流式拼接,绝无内存重复搬移)
pub fn rewrite_source(
  source: &str,
  paths: &[QualifiedPath],
  insert_pos: usize,
  scope: &FileScope,
  options: &Options,
) -> Option<RewriteResult> {
  let line_offsets = compute_line_offsets(source);
  rewrite_source_with_offsets(source, &line_offsets, paths, insert_pos, scope, options)
}

/// 接收外部已计算好的行偏移切片,彻底消除跨模块重复行扫描
pub fn rewrite_source_with_offsets(
  source: &str,
  line_offsets: &[usize],
  paths: &[QualifiedPath],
  insert_pos: usize,
  scope: &FileScope,
  _options: &Options,
) -> Option<RewriteResult> {
  if paths.is_empty() {
    return None;
  }

  let mut actions = Vec::with_capacity(paths.len());
  let mut needed_imports = HashSet::default();
  let mut new_in_scope = HashSet::default();

  let is_in_scope = |ident: &str, new_in_scope: &HashSet<String>| {
    scope.in_scope_idents.contains(ident) || new_in_scope.contains(ident)
  };

  // 预检导入冲突:若同一文件出现多个不同来源的同名末尾项(如 core::fmt::Error 与 std::io::Error)
  let conflicting_tails: HashSet<&str> = if paths.len() > 1 {
    let mut tail_to_import: HashMap<&str, &str> = HashMap::default();
    let mut conflicts = HashSet::default();
    for qp in paths {
      let tail = qp.last_ident();
      let import = qp.import.as_str();
      match tail_to_import.entry(tail) {
        Entry::Vacant(e) => {
          e.insert(import);
        }
        Entry::Occupied(e) => {
          if *e.get() != import {
            conflicts.insert(tail);
          }
        }
      }
    }
    conflicts
  } else {
    HashSet::default()
  };

  for qp in paths {
    let span_start = qp.span.start();
    if span_start.line == 0 || span_start.line > line_offsets.len() {
      continue;
    }

    let line_start = line_offsets[span_start.line - 1];
    let line_end = if span_start.line < line_offsets.len() {
      line_offsets[span_start.line]
    } else {
      source.len()
    };

    let line_text = &source[line_start..line_end];
    let target = &qp.original;
    let trimmed_target = target.trim_start_matches(':');

    // 计算 span 列对应的字符/字节起始偏移
    let start_search = line_text
      .char_indices()
      .nth(span_start.column)
      .map(|(idx, _)| idx)
      .unwrap_or(span_start.column.min(line_text.len()));

    let match_info = if line_text[start_search..].starts_with(target) {
      Some((start_search, target.len()))
    } else if line_text[start_search..].starts_with(trimmed_target) {
      Some((start_search, trimmed_target.len()))
    } else if let Some(pos) = line_text[start_search..].find(target) {
      Some((start_search + pos, target.len()))
    } else if let Some(pos) = line_text[start_search..].find(trimmed_target) {
      Some((start_search + pos, trimmed_target.len()))
    } else if let Some(pos) = line_text.find(target) {
      Some((pos, target.len()))
    } else {
      line_text
        .find(trimmed_target)
        .map(|pos| (pos, trimmed_target.len()))
    };

    let Some((col_start, matched_len)) = match_info else {
      continue;
    };

    let start_byte = line_start + col_start;
    let end_byte = start_byte + matched_len;

    // 严谨校验切片范围
    if end_byte <= source.len() {
      let mut replacement = qp.replacement.clone();
      let mut import = qp.import.clone();
      let total = qp.segments.len();

      if conflicting_tails.contains(qp.last_ident()) && total >= 2 {
        let parent_mod = &qp.segments[total - 2];
        if is_in_scope(parent_mod.as_str(), &new_in_scope) {
          continue;
        }
        import = qp.segments[..total - 1].join("::");
        replacement = qp.segments[total - 2..].join("::");
      }

      let import_tail = import.rsplit("::").next().unwrap_or_default();

      if scope.existing_use_paths.contains(&import) || needed_imports.contains(&import) {
        // 该项已经在文件顶层 use 中引入,或本轮已规划引入相同路径,直接使用 short name
      } else if is_in_scope(import_tail, &new_in_scope) {
        // 该符号已被本地同名定义或不同来源的 use 占用,尝试保留上一级模块以避免冲突
        if total >= 2 {
          let parent_mod = &qp.segments[total - 2];
          let candidate_import = qp.segments[..total - 1].join("::");
          let candidate_tail = parent_mod.as_str();

          if scope.existing_use_paths.contains(&candidate_import)
            || needed_imports.contains(&candidate_import)
          {
            replacement = qp.segments[total - 2..].join("::");
          } else if !is_in_scope(candidate_tail, &new_in_scope) {
            import = candidate_import;
            replacement = qp.segments[total - 2..].join("::");
            needed_imports.insert(import.clone());
            new_in_scope.insert(candidate_tail.to_string());
          } else {
            // 上一级模块名亦冲突,跳过以避免破坏代码
            continue;
          }
        } else {
          continue;
        }
      } else {
        needed_imports.insert(import.clone());
        new_in_scope.insert(import_tail.to_string());
      }

      actions.push(ReplaceAction {
        start: start_byte,
        end: end_byte,
        original: qp.original.clone(),
        replacement,
      });
    }
  }

  if actions.is_empty() {
    return None;
  }

  // 排序并原地去重叠
  actions.sort_unstable_by(|a, b| a.start.cmp(&b.start).then_with(|| b.end.cmp(&a.end)));

  let mut last_end = 0;
  actions.retain(|act| {
    if act.start >= last_end {
      last_end = act.end;
      true
    } else {
      false
    }
  });

  let mut to_insert: Vec<String> = needed_imports
    .into_iter()
    .filter(|imp| !scope.existing_use_paths.contains(imp))
    .collect();

  let mut use_block = String::new();
  if !to_insert.is_empty() {
    to_insert.sort_unstable();
    let total_len: usize = to_insert.iter().map(|s| s.len() + 6).sum::<usize>() + 2;
    use_block.reserve(total_len);
    for imp in &to_insert {
      use_block.push_str("use ");
      use_block.push_str(imp);
      use_block.push_str(";\n");
    }
    if insert_pos < source.len() && !source[insert_pos..].starts_with('\n') {
      use_block.push('\n');
    }
  }

  // 若既没有实质替换,也没有注入 use 块,直接返回 None,避免无谓重写与触发外部 rustfmt
  if actions.is_empty() && use_block.is_empty() {
    return None;
  }

  // 单次线性流动重组拼接:同时完成 replacement 替换与 insert_pos 处的 use 注入
  let mut current = String::with_capacity(source.len() + use_block.len() + 128);
  let mut cur_idx = 0;
  let mut use_inserted = use_block.is_empty();
  let mut replacements = Vec::with_capacity(actions.len());

  for act in actions {
    // 检查 use 插入点是否位于当前区间之前
    if !use_inserted && insert_pos <= act.start {
      if insert_pos > cur_idx {
        current.push_str(&source[cur_idx..insert_pos]);
        cur_idx = insert_pos;
      }
      current.push_str(&use_block);
      use_inserted = true;
    }

    if act.start > cur_idx {
      current.push_str(&source[cur_idx..act.start]);
    }
    current.push_str(&act.replacement);
    cur_idx = act.end;

    replacements.push((act.original, act.replacement));
  }

  // 若尚未到达 use 插入点
  if !use_inserted {
    if insert_pos > cur_idx {
      current.push_str(&source[cur_idx..insert_pos]);
      cur_idx = insert_pos;
    }
    current.push_str(&use_block);
  }

  current.push_str(&source[cur_idx..]);

  if current == source {
    return None;
  }

  Some(RewriteResult {
    content: current,
    replacements,
  })
}