use alloc::string::String;
use core::fmt::Write as _;
use hir_lang::{BinderId, DefId, HirError, Name, NodeRef, Ns, PathRoot, Res, Span, UnitId, Vis};
use intern_lang::Lookup;
use crate::env::DefKind;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum DiagKind {
Unresolved {
name: Name,
ns: Ns,
container: Option<Name>,
suggestion: Option<Name>,
},
AmbiguousGlob {
name: Name,
first: Res,
second: Res,
},
Private {
name: Name,
res: Res,
vis: Vis,
},
Duplicate {
name: Name,
first: Span,
},
CannotCapture {
name: Name,
binder: BinderId,
},
WrongKind {
name: Name,
ns: Ns,
found: DefKind,
},
NotAContainer {
name: Name,
found: DefKind,
},
Shadowing {
name: Name,
shadowed: BinderId,
},
BrokenImport {
name: Name,
},
ImportCycle {
name: Name,
},
ImportAmbiguity {
name: Name,
},
GlobsUnsupported,
AliasesUnsupported,
RootUnsupported {
root: PathRoot,
},
OutsideType {
root: PathRoot,
},
NoParent,
SuperBeyondRoot {
levels: u8,
},
MixinConflict {
name: Name,
first: DefId,
second: DefId,
},
NotAMixin {
found: DefKind,
},
UnknownMixinMember {
name: hir_lang::Symbol,
},
MixinCycle,
InconsistentMro {
class: Name,
},
Rejected {
error: HirError,
},
}
impl DiagKind {
#[must_use]
pub const fn is_unresolved(&self) -> bool {
matches!(self, Self::Unresolved { .. })
}
#[must_use]
pub const fn suggestion(&self) -> Option<Name> {
match self {
Self::Unresolved { suggestion, .. } => *suggestion,
_ => None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Diagnostic {
pub kind: DiagKind,
pub unit: UnitId,
pub span: Span,
pub node: Option<NodeRef>,
}
impl Diagnostic {
#[must_use]
pub fn message<L: Lookup>(&self, names: &L) -> String {
let mut out = String::new();
let n = |name: Name| -> String {
names
.resolve_with(name.sym, |s: &str| String::from(s))
.unwrap_or_else(|| String::from("?"))
};
let _infallible = match self.kind {
DiagKind::Unresolved {
name,
ns,
container,
suggestion,
} => {
let r = match container {
Some(c) => write!(out, "cannot find `{}` in `{}`", n(name), n(c)),
None => write!(
out,
"cannot find {} `{}` in this scope",
ns_word(ns),
n(name)
),
};
match suggestion {
Some(s) => write!(out, "; did you mean `{}`?", n(s)),
None => r,
}
}
DiagKind::AmbiguousGlob { name, .. } => write!(
out,
"`{}` is ambiguous: more than one glob import provides it",
n(name)
),
DiagKind::Private { name, vis, .. } => {
write!(out, "`{}` is {}", n(name), vis_word(vis))
}
DiagKind::Duplicate { name, .. } => {
write!(out, "`{}` is defined more than once in this scope", n(name))
}
DiagKind::CannotCapture { name, .. } => write!(
out,
"cannot use local `{}` here: it belongs to an enclosing function",
n(name)
),
DiagKind::WrongKind { name, ns, found } => write!(
out,
"expected {}, found {} `{}`",
expected_word(ns),
found.name(),
n(name)
),
DiagKind::NotAContainer { name, found } => write!(
out,
"`{}` is a {}, which has no members",
n(name),
found.name()
),
DiagKind::Shadowing { name, .. } => write!(
out,
"`{}` shadows a visible local, which this language forbids",
n(name)
),
DiagKind::BrokenImport { name } => write!(
out,
"`{}` comes from an import that failed to resolve",
n(name)
),
DiagKind::ImportCycle { name } => {
write!(out, "the import of `{}` depends on itself", n(name))
}
DiagKind::ImportAmbiguity { name } => write!(
out,
"the import of `{}` is ambiguous with a glob import in the same scope",
n(name)
),
DiagKind::GlobsUnsupported => {
write!(out, "glob imports are not supported by this language")
}
DiagKind::AliasesUnsupported => {
write!(out, "import aliases are not supported by this language")
}
DiagKind::RootUnsupported { root } => {
write!(out, "`{}` is not supported here", root_word(root))
}
DiagKind::OutsideType { root } => write!(
out,
"`{}` used outside a class, interface, or impl",
root_word(root)
),
DiagKind::NoParent => write!(out, "`parent::` used in a class with no base class"),
DiagKind::SuperBeyondRoot { levels } => write!(
out,
"`super::` {} level(s) up goes past the root module",
levels
),
DiagKind::MixinConflict { name, .. } => write!(
out,
"more than one mixin provides `{}`; choose one with `insteadof`",
n(name)
),
DiagKind::NotAMixin { found } => {
write!(out, "expected a mixin, found {}", found.name())
}
DiagKind::UnknownMixinMember { name } => write!(
out,
"no mixin used here has a member `{}`",
n(Name::new(name))
),
DiagKind::MixinCycle => write!(out, "this mixin uses itself"),
DiagKind::InconsistentMro { class } => write!(
out,
"cannot create a consistent method resolution order (MRO) for the bases of `{}`",
n(class)
),
DiagKind::Rejected { error } => {
write!(out, "the HIR refused this resolution: {error}")
}
};
out
}
}
const fn ns_word(ns: Ns) -> &'static str {
match ns {
Ns::Value => "value",
Ns::Type => "type",
Ns::Pattern => "pattern",
Ns::Region => "region",
Ns::Import => "import",
}
}
const fn expected_word(ns: Ns) -> &'static str {
match ns {
Ns::Value => "a value",
Ns::Type => "a type",
Ns::Pattern => "a constant, variant, or record",
Ns::Region => "a region",
Ns::Import => "an importable item",
}
}
const fn vis_word(vis: Vis) -> &'static str {
match vis {
Vis::Private => "private",
Vis::Protected => "protected",
Vis::Package => "package-private",
Vis::Public => "public",
}
}
const fn root_word(root: PathRoot) -> &'static str {
match root {
PathRoot::Relative => "a relative path",
PathRoot::Global => "::",
PathRoot::SelfModule => "self::",
PathRoot::Super(_) => "super::",
PathRoot::SelfType => "self::",
PathRoot::ParentType => "parent::",
PathRoot::StaticType => "static::",
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use intern_lang::Interner;
fn diag(kind: DiagKind) -> Diagnostic {
Diagnostic {
kind,
unit: UnitId::new(0),
span: Span::new(0, 1),
node: None,
}
}
#[test]
fn test_every_kind_renders_a_message() {
let mut names = Interner::new();
let x = Name::new(names.intern("x"));
let b = BinderId::from_index(0).unwrap();
let d = DefId::foreign(
UnitId::new(1),
hir_lang::Def::Item(hir_lang::ItemId::from_index(0).unwrap()),
);
let kinds = [
DiagKind::Unresolved {
name: x,
ns: Ns::Type,
container: Some(x),
suggestion: None,
},
DiagKind::AmbiguousGlob {
name: x,
first: Res::Err,
second: Res::Err,
},
DiagKind::Private {
name: x,
res: Res::Err,
vis: Vis::Protected,
},
DiagKind::Duplicate {
name: x,
first: Span::new(0, 1),
},
DiagKind::CannotCapture { name: x, binder: b },
DiagKind::WrongKind {
name: x,
ns: Ns::Pattern,
found: DefKind::Fn,
},
DiagKind::NotAContainer {
name: x,
found: DefKind::Fn,
},
DiagKind::Shadowing {
name: x,
shadowed: b,
},
DiagKind::BrokenImport { name: x },
DiagKind::ImportCycle { name: x },
DiagKind::ImportAmbiguity { name: x },
DiagKind::GlobsUnsupported,
DiagKind::AliasesUnsupported,
DiagKind::RootUnsupported {
root: PathRoot::StaticType,
},
DiagKind::OutsideType {
root: PathRoot::SelfType,
},
DiagKind::NoParent,
DiagKind::SuperBeyondRoot { levels: 2 },
DiagKind::MixinConflict {
name: x,
first: d,
second: d,
},
DiagKind::NotAMixin {
found: DefKind::Sum,
},
DiagKind::UnknownMixinMember { name: x.sym },
DiagKind::MixinCycle,
DiagKind::InconsistentMro { class: x },
DiagKind::Rejected {
error: HirError::RootNotModule,
},
];
for k in kinds {
let m = diag(k).message(&names);
assert!(!m.is_empty());
assert!(!m.contains('?') || k.is_unresolved(), "{m}");
}
assert_eq!(diag(kinds[0]).message(&names), "cannot find `x` in `x`");
assert_eq!(diag(kinds[2]).message(&names), "`x` is protected");
}
#[test]
fn test_message_survives_unknown_symbol() {
let names = Interner::new();
let ghost = Name::new(hir_lang::Symbol::from_u32(77).unwrap());
let m = diag(DiagKind::BrokenImport { name: ghost }).message(&names);
assert!(m.contains("`?`"));
}
}