#![doc = include_str!("../README.md")]
use std::fmt::Write as _;
use std::path::PathBuf;
mod cache;
mod config;
mod deps;
mod entry;
mod error;
mod expand;
#[cfg(feature = "fuzzing")]
#[doc(hidden)]
pub mod fuzzing;
mod git;
mod index;
mod intern;
mod manifest;
mod output;
mod parse;
mod scan;
mod serde_names;
mod stdlib;
mod suggest;
mod text;
pub mod vocabulary;
pub mod web;
pub use error::Error;
pub(crate) use hashbrown::{HashMap, HashSet};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Range {
Full,
Since(String),
Base {
base: String,
head: Option<String>,
},
}
#[derive(Debug, Clone)]
pub struct Options {
pub root: PathBuf,
pub range: Range,
pub cache_dir: Option<PathBuf>,
pub config: Option<PathBuf>,
pub package: Option<String>,
}
impl Options {
#[must_use]
pub fn new(root: PathBuf) -> Self {
Self {
root,
range: Range::Full,
cache_dir: None,
config: None,
package: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ItemKind {
Module,
Struct,
Enum,
Union,
Trait,
TraitAlias,
TypeAlias,
Const,
Static,
Function,
Macro,
Variant,
Field,
Method,
AssociatedConst,
AssociatedType,
Reexport,
Package,
Dependency,
Feature,
Binary,
Example,
Serialized,
}
impl ItemKind {
#[must_use]
pub fn describe(self) -> &'static str {
match self {
Self::Module => "module",
Self::Struct => "struct",
Self::Enum => "enum",
Self::Union => "union",
Self::Trait => "trait",
Self::TraitAlias => "trait alias",
Self::TypeAlias => "type alias",
Self::Const => "const",
Self::Static => "static",
Self::Function => "function",
Self::Macro => "macro",
Self::Variant => "variant",
Self::Field => "field",
Self::Method => "method",
Self::AssociatedConst => "associated const",
Self::AssociatedType => "associated type",
Self::Reexport => "re-export",
Self::Package => "package",
Self::Dependency => "dependency",
Self::Feature => "feature",
Self::Binary => "binary",
Self::Example => "example",
Self::Serialized => "serialized name",
}
}
#[must_use]
pub fn is_manifest_entry(self) -> bool {
matches!(
self,
Self::Package | Self::Dependency | Self::Feature | Self::Binary | Self::Example
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Source {
Comment,
DocComment,
Markdown,
StringLiteral,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Kind {
Text,
CodeSpan,
CodeBlock {
compiled: bool,
},
Link,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
pub struct Context {
pub source: Source,
pub kind: Kind,
}
impl Context {
#[must_use]
pub fn describe(self) -> String {
let source = match self.source {
Source::Comment => "comment",
Source::DocComment => "doc comment",
Source::Markdown => "Markdown",
Source::StringLiteral => "string literal",
};
let kind = match self.kind {
Kind::Text => "",
Kind::CodeSpan => " code span",
Kind::CodeBlock { compiled: true } => " code block",
Kind::CodeBlock { compiled: false } => " uncompiled code block",
Kind::Link => " link",
};
format!("{source}{kind}")
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RemovedIn {
Commit(String),
WorkingTree,
}
impl serde::Serialize for RemovedIn {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
Self::Commit(id) => serializer.serialize_str(id),
Self::WorkingTree => serializer.serialize_str("working_tree"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
pub struct Mention {
pub file: String,
pub line: usize,
pub column: usize,
pub text: String,
pub context: Context,
pub replacement: Option<String>,
pub uppercase: Option<Uppercase>,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
pub struct Uppercase {
pub spelling: String,
pub vocabulary: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Change {
Moved,
Renamed,
Replaced,
}
impl Change {
#[must_use]
pub fn describe(self) -> &'static str {
match self {
Self::Moved => "moved to",
Self::Renamed => "renamed to",
Self::Replaced => "replaced by",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Suggestion {
pub path: String,
pub change: Change,
pub declared_at: Option<Position>,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
pub struct Position {
pub file: String,
pub line: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Group {
pub path: String,
pub kind: ItemKind,
pub member: Option<String>,
pub removed_in: Option<RemovedIn>,
pub declared_at: Option<Position>,
pub suggestion: Option<Suggestion>,
pub candidates: Vec<String>,
pub mentions: Vec<Mention>,
}
impl serde::Serialize for Group {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeStruct as _;
let mut group = serializer.serialize_struct("Group", 10)?;
group.serialize_field("path", &self.path)?;
group.serialize_field("kind", &self.kind)?;
group.serialize_field("member", &self.member)?;
group.serialize_field("removed_in", &self.removed_in)?;
group.serialize_field("declared_at", &self.declared_at)?;
group.serialize_field("suggestion", &self.suggestion.as_ref().map(|s| &s.path))?;
group.serialize_field(
"suggested_at",
&self
.suggestion
.as_ref()
.and_then(|s| s.declared_at.as_ref()),
)?;
group.serialize_field("candidates", &self.candidates)?;
group.serialize_field("headline", &self.headline())?;
group.serialize_field("mentions", &self.mentions)?;
group.end()
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize)]
pub struct Report {
pub base: Option<String>,
pub head: Option<String>,
pub groups: Vec<Group>,
pub skipped: Vec<Skipped>,
pub unused_markers: Vec<UnusedMarker>,
#[serde(skip)]
edited: HashSet<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
pub struct UnusedMarker {
pub file: String,
pub line: usize,
pub column: usize,
pub name: String,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
pub struct Skipped {
pub dependency: String,
pub reason: String,
}
impl Report {
#[must_use]
pub fn is_clean(&self) -> bool {
self.groups.is_empty() && self.unused_markers.is_empty()
}
#[must_use]
pub fn human(&self) -> String {
let mut out = self.findings();
for marker in &self.unused_markers {
let _ = writeln!(
out,
"{}:{}:{} refquiem: allow({}) suppresses nothing",
marker.file, marker.line, marker.column, marker.name
);
}
for skipped in &self.skipped {
let reason = skipped
.reason
.lines()
.find(|line| line.starts_with("error:"))
.or_else(|| skipped.reason.lines().next())
.unwrap_or_default();
let _ = writeln!(
out,
"warning: removed dependency `{}` could not be resolved, so its items were not checked: {reason}",
skipped.dependency
);
}
out
}
fn findings(&self) -> String {
if self.groups.is_empty() {
return "refquiem: no stale mentions\n".to_owned();
}
let mut out = format!("refquiem: {}\n", self.tally());
for group in &self.groups {
let _ = writeln!(out, "\n{}", group.headline());
for mention in &group.mentions {
let _ = write!(
out,
" {}:{}:{} {}",
mention.file,
mention.line,
mention.column,
mention.context.describe()
);
if let Some(hint) = &mention.uppercase {
let _ = write!(
out,
", write {} to mark it as {}",
hint.spelling, hint.vocabulary
);
}
out.push('\n');
}
}
out
}
fn tally(&self) -> String {
let mentions: usize = self.groups.iter().map(|g| g.mentions.len()).sum();
format!(
"{} still mentioned, {}",
plural(self.groups.len(), "removed name", "removed names"),
plural(mentions, "mention", "mentions"),
)
}
}
impl Group {
fn headline(&self) -> String {
let mut out = format!("`{}` ({}", self.path, self.kind.describe());
if let Some(member) = &self.member {
let _ = write!(out, " of `{member}`");
}
out.push(')');
let verb = if self.kind.is_manifest_entry() {
"removed"
} else {
"deleted"
};
match &self.removed_in {
Some(RemovedIn::Commit(id)) => {
let _ = write!(out, ", {verb} in {}", id.get(..7).unwrap_or(id));
}
Some(RemovedIn::WorkingTree) => {
let _ = write!(out, ", {verb} in the working tree");
}
None => {}
}
if let Some(became) = self.became() {
let _ = write!(out, ", {became}");
}
out
}
fn became(&self) -> Option<String> {
if let Some(suggestion) = &self.suggestion {
return Some(format!(
"{} `{}`",
suggestion.change.describe(),
suggestion.path
));
}
let (last, rest) = self.candidates.split_last()?;
let rest: Vec<String> = rest.iter().map(|c| format!("`{c}`")).collect();
Some(format!("perhaps {} or `{last}`", rest.join(", ")))
}
}
fn plural(count: usize, one: &str, many: &str) -> String {
format!("{count} {}", if count == 1 { one } else { many })
}
const SCAN_STACK: usize = 256 << 20;
pub fn run(options: &Options) -> Result<Report, Error> {
std::thread::scope(|scope| {
std::thread::Builder::new()
.name("refquiem".to_owned())
.stack_size(SCAN_STACK)
.spawn_scoped(scope, || scan::run(options))
.map_err(Error::Thread)?
.join()
.unwrap_or_else(|panic| std::panic::resume_unwind(panic))
})
}