resolve_lang/diag.rs
1//! Diagnostics: what resolution reports, with did-you-mean suggestions.
2
3use alloc::string::String;
4use core::fmt::Write as _;
5
6use hir_lang::{BinderId, DefId, HirError, Name, NodeRef, Ns, PathRoot, Res, Span, UnitId, Vis};
7use intern_lang::Lookup;
8
9use crate::env::DefKind;
10
11/// What went wrong, with the data a tool needs to explain or fix it.
12///
13/// # Examples
14///
15/// ```
16/// use hir_lang::{Name, Ns, Symbol};
17/// use resolve_lang::DiagKind;
18///
19/// let x = Name::new(Symbol::from_u32(1).unwrap());
20/// let k = DiagKind::Unresolved { name: x, ns: Ns::Value, container: None, suggestion: None };
21/// assert!(k.is_unresolved());
22/// ```
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24#[non_exhaustive]
25pub enum DiagKind {
26 /// A name that is not visible here (or, with `container`, that the
27 /// container does not define). `suggestion` is the closest visible name
28 /// within a small edit distance, if any.
29 Unresolved {
30 /// The name.
31 name: Name,
32 /// The syntactic namespace of the path.
33 ns: Ns,
34 /// The module or type the name was looked up in, for `m::x`.
35 container: Option<Name>,
36 /// The did-you-mean candidate.
37 suggestion: Option<Name>,
38 },
39 /// A name more than one glob import provides, with different meanings.
40 AmbiguousGlob {
41 /// The name.
42 name: Name,
43 /// One meaning.
44 first: Res,
45 /// Another meaning.
46 second: Res,
47 },
48 /// A name that exists but is not visible from here. The path is still
49 /// resolved to it, so tools can navigate.
50 Private {
51 /// The name.
52 name: Name,
53 /// What it resolved to.
54 res: Res,
55 /// Its visibility.
56 vis: Vis,
57 },
58 /// A second definition of a name in one scope and namespace.
59 Duplicate {
60 /// The name.
61 name: Name,
62 /// Where the first definition's name is.
63 first: Span,
64 },
65 /// A local of an enclosing function used where it cannot be captured
66 /// (inside a nested item, or past a frame that does not capture).
67 CannotCapture {
68 /// The name.
69 name: Name,
70 /// The local it would have named.
71 binder: BinderId,
72 },
73 /// A definition of the wrong kind for its position (a function used as a
74 /// type, a local named by a constructor pattern).
75 WrongKind {
76 /// The name.
77 name: Name,
78 /// The syntactic namespace of the path.
79 ns: Ns,
80 /// What the name actually is.
81 found: DefKind,
82 },
83 /// A path that continues past something without members (`f::x` where
84 /// `f` is a function, or a variant used as a prefix).
85 NotAContainer {
86 /// The name of the non-container.
87 name: Name,
88 /// What it is.
89 found: DefKind,
90 },
91 /// A binder that reuses a visible name the policy forbids shadowing.
92 Shadowing {
93 /// The name.
94 name: Name,
95 /// The binder it shadows.
96 shadowed: BinderId,
97 },
98 /// A name whose import failed to resolve (the import itself has its own
99 /// diagnostic).
100 BrokenImport {
101 /// The name.
102 name: Name,
103 },
104 /// An import that depends on itself through other imports.
105 ImportCycle {
106 /// The name the import binds.
107 name: Name,
108 },
109 /// An import that resolved while a glob import of the same scope was
110 /// still undetermined, and that the finished glob makes ambiguous.
111 ImportAmbiguity {
112 /// The name.
113 name: Name,
114 },
115 /// A glob import in a language without them.
116 GlobsUnsupported,
117 /// An aliased import in a language without them.
118 AliasesUnsupported,
119 /// A path root the language does not have, or that cannot start an import.
120 RootUnsupported {
121 /// The root.
122 root: PathRoot,
123 },
124 /// `self::`, `parent::`, or `static::` outside any class, interface, or impl.
125 OutsideType {
126 /// The root.
127 root: PathRoot,
128 },
129 /// `parent::` in a class with no base class.
130 NoParent,
131 /// `super::` past the unit's root module.
132 SuperBeyondRoot {
133 /// The number of levels asked for.
134 levels: u8,
135 },
136 /// Two mixins provide the same member and no `insteadof` rule chooses.
137 MixinConflict {
138 /// The member name.
139 name: Name,
140 /// The member taken (the first mixin's).
141 first: DefId,
142 /// The member dropped.
143 second: DefId,
144 },
145 /// Something that is not a mixin used as one.
146 NotAMixin {
147 /// What it is.
148 found: DefKind,
149 },
150 /// A mixin rule naming a member no used mixin has.
151 UnknownMixinMember {
152 /// The member name.
153 name: hir_lang::Symbol,
154 },
155 /// A mixin that uses itself, directly or through other mixins.
156 MixinCycle,
157 /// The HIR refused a resolution resolve-lang computed. This indicates a
158 /// disagreement between this crate and hir-lang's checks; the path is
159 /// left as `Res::Err` so the `Hir` stays valid.
160 Rejected {
161 /// The refusal.
162 error: HirError,
163 },
164}
165
166impl DiagKind {
167 /// Whether this is an unresolved-name diagnostic.
168 ///
169 /// # Examples
170 ///
171 /// ```
172 /// use resolve_lang::DiagKind;
173 ///
174 /// assert!(!DiagKind::NoParent.is_unresolved());
175 /// ```
176 #[must_use]
177 pub const fn is_unresolved(&self) -> bool {
178 matches!(self, Self::Unresolved { .. })
179 }
180
181 /// The did-you-mean suggestion, if this diagnostic has one.
182 ///
183 /// # Examples
184 ///
185 /// ```
186 /// use hir_lang::{Name, Ns, Symbol};
187 /// use resolve_lang::DiagKind;
188 ///
189 /// let (x, y) = (Name::new(Symbol::from_u32(1).unwrap()), Name::new(Symbol::from_u32(2).unwrap()));
190 /// let k = DiagKind::Unresolved { name: x, ns: Ns::Value, container: None, suggestion: Some(y) };
191 /// assert_eq!(k.suggestion(), Some(y));
192 /// ```
193 #[must_use]
194 pub const fn suggestion(&self) -> Option<Name> {
195 match self {
196 Self::Unresolved { suggestion, .. } => *suggestion,
197 _ => None,
198 }
199 }
200}
201
202/// One problem found while resolving, located in a unit.
203///
204/// # Examples
205///
206/// ```
207/// use hir_lang::{Span, UnitId};
208/// use intern_lang::Interner;
209/// use resolve_lang::{DiagKind, Diagnostic};
210///
211/// let d = Diagnostic { kind: DiagKind::NoParent, unit: UnitId::new(0), span: Span::new(3, 9), node: None };
212/// assert_eq!(d.message(&Interner::new()), "`parent::` used in a class with no base class");
213/// ```
214#[derive(Clone, Copy, Debug, PartialEq, Eq)]
215pub struct Diagnostic {
216 /// What went wrong.
217 pub kind: DiagKind,
218 /// The unit it is in.
219 pub unit: UnitId,
220 /// Where: the offending name's source span (for an expanded name, the
221 /// span its origin records).
222 pub span: Span,
223 /// The HIR node it is about: usually the path, or the item for
224 /// definitions and mixins.
225 pub node: Option<NodeRef>,
226}
227
228impl Diagnostic {
229 /// A one-line English message, with names spelled through `names`.
230 ///
231 /// # Examples
232 ///
233 /// ```
234 /// use hir_lang::{Name, Ns, Span, UnitId};
235 /// use intern_lang::Interner;
236 /// use resolve_lang::{DiagKind, Diagnostic};
237 ///
238 /// let mut names = Interner::new();
239 /// let (lenght, length) = (Name::new(names.intern("lenght")), Name::new(names.intern("length")));
240 /// let d = Diagnostic {
241 /// kind: DiagKind::Unresolved { name: lenght, ns: Ns::Value, container: None, suggestion: Some(length) },
242 /// unit: UnitId::new(0),
243 /// span: Span::new(0, 6),
244 /// node: None,
245 /// };
246 /// assert_eq!(d.message(&names), "cannot find value `lenght` in this scope; did you mean `length`?");
247 /// ```
248 #[must_use]
249 pub fn message<L: Lookup>(&self, names: &L) -> String {
250 let mut out = String::new();
251 let n = |name: Name| -> String {
252 names
253 .resolve_with(name.sym, |s: &str| String::from(s))
254 .unwrap_or_else(|| String::from("?"))
255 };
256 // Writing into a String cannot fail; the results are discarded on
257 // purpose (`fmt::Write for String` is infallible).
258 let _infallible = match self.kind {
259 DiagKind::Unresolved {
260 name,
261 ns,
262 container,
263 suggestion,
264 } => {
265 let r = match container {
266 Some(c) => write!(out, "cannot find `{}` in `{}`", n(name), n(c)),
267 None => write!(
268 out,
269 "cannot find {} `{}` in this scope",
270 ns_word(ns),
271 n(name)
272 ),
273 };
274 match suggestion {
275 Some(s) => write!(out, "; did you mean `{}`?", n(s)),
276 None => r,
277 }
278 }
279 DiagKind::AmbiguousGlob { name, .. } => write!(
280 out,
281 "`{}` is ambiguous: more than one glob import provides it",
282 n(name)
283 ),
284 DiagKind::Private { name, vis, .. } => {
285 write!(out, "`{}` is {}", n(name), vis_word(vis))
286 }
287 DiagKind::Duplicate { name, .. } => {
288 write!(out, "`{}` is defined more than once in this scope", n(name))
289 }
290 DiagKind::CannotCapture { name, .. } => write!(
291 out,
292 "cannot use local `{}` here: it belongs to an enclosing function",
293 n(name)
294 ),
295 DiagKind::WrongKind { name, ns, found } => write!(
296 out,
297 "expected {}, found {} `{}`",
298 expected_word(ns),
299 found.name(),
300 n(name)
301 ),
302 DiagKind::NotAContainer { name, found } => write!(
303 out,
304 "`{}` is a {}, which has no members",
305 n(name),
306 found.name()
307 ),
308 DiagKind::Shadowing { name, .. } => write!(
309 out,
310 "`{}` shadows a visible local, which this language forbids",
311 n(name)
312 ),
313 DiagKind::BrokenImport { name } => write!(
314 out,
315 "`{}` comes from an import that failed to resolve",
316 n(name)
317 ),
318 DiagKind::ImportCycle { name } => {
319 write!(out, "the import of `{}` depends on itself", n(name))
320 }
321 DiagKind::ImportAmbiguity { name } => write!(
322 out,
323 "the import of `{}` is ambiguous with a glob import in the same scope",
324 n(name)
325 ),
326 DiagKind::GlobsUnsupported => {
327 write!(out, "glob imports are not supported by this language")
328 }
329 DiagKind::AliasesUnsupported => {
330 write!(out, "import aliases are not supported by this language")
331 }
332 DiagKind::RootUnsupported { root } => {
333 write!(out, "`{}` is not supported here", root_word(root))
334 }
335 DiagKind::OutsideType { root } => write!(
336 out,
337 "`{}` used outside a class, interface, or impl",
338 root_word(root)
339 ),
340 DiagKind::NoParent => write!(out, "`parent::` used in a class with no base class"),
341 DiagKind::SuperBeyondRoot { levels } => write!(
342 out,
343 "`super::` {} level(s) up goes past the root module",
344 levels
345 ),
346 DiagKind::MixinConflict { name, .. } => write!(
347 out,
348 "more than one mixin provides `{}`; choose one with `insteadof`",
349 n(name)
350 ),
351 DiagKind::NotAMixin { found } => {
352 write!(out, "expected a mixin, found {}", found.name())
353 }
354 DiagKind::UnknownMixinMember { name } => write!(
355 out,
356 "no mixin used here has a member `{}`",
357 n(Name::new(name))
358 ),
359 DiagKind::MixinCycle => write!(out, "this mixin uses itself"),
360 DiagKind::Rejected { error } => {
361 write!(out, "the HIR refused this resolution: {error}")
362 }
363 };
364 out
365 }
366}
367
368const fn ns_word(ns: Ns) -> &'static str {
369 match ns {
370 Ns::Value => "value",
371 Ns::Type => "type",
372 Ns::Pattern => "pattern",
373 Ns::Region => "region",
374 Ns::Import => "import",
375 }
376}
377
378const fn expected_word(ns: Ns) -> &'static str {
379 match ns {
380 Ns::Value => "a value",
381 Ns::Type => "a type",
382 Ns::Pattern => "a constant, variant, or record",
383 Ns::Region => "a region",
384 Ns::Import => "an importable item",
385 }
386}
387
388const fn vis_word(vis: Vis) -> &'static str {
389 match vis {
390 Vis::Private => "private",
391 Vis::Protected => "protected",
392 Vis::Package => "package-private",
393 Vis::Public => "public",
394 }
395}
396
397const fn root_word(root: PathRoot) -> &'static str {
398 match root {
399 PathRoot::Relative => "a relative path",
400 PathRoot::Global => "::",
401 PathRoot::SelfModule => "self::",
402 PathRoot::Super(_) => "super::",
403 PathRoot::SelfType => "self::",
404 PathRoot::ParentType => "parent::",
405 PathRoot::StaticType => "static::",
406 }
407}
408
409#[cfg(test)]
410#[allow(clippy::unwrap_used)]
411mod tests {
412 use super::*;
413 use intern_lang::Interner;
414
415 fn diag(kind: DiagKind) -> Diagnostic {
416 Diagnostic {
417 kind,
418 unit: UnitId::new(0),
419 span: Span::new(0, 1),
420 node: None,
421 }
422 }
423
424 #[test]
425 fn test_every_kind_renders_a_message() {
426 let mut names = Interner::new();
427 let x = Name::new(names.intern("x"));
428 let b = BinderId::from_index(0).unwrap();
429 let d = DefId::foreign(
430 UnitId::new(1),
431 hir_lang::Def::Item(hir_lang::ItemId::from_index(0).unwrap()),
432 );
433 let kinds = [
434 DiagKind::Unresolved {
435 name: x,
436 ns: Ns::Type,
437 container: Some(x),
438 suggestion: None,
439 },
440 DiagKind::AmbiguousGlob {
441 name: x,
442 first: Res::Err,
443 second: Res::Err,
444 },
445 DiagKind::Private {
446 name: x,
447 res: Res::Err,
448 vis: Vis::Protected,
449 },
450 DiagKind::Duplicate {
451 name: x,
452 first: Span::new(0, 1),
453 },
454 DiagKind::CannotCapture { name: x, binder: b },
455 DiagKind::WrongKind {
456 name: x,
457 ns: Ns::Pattern,
458 found: DefKind::Fn,
459 },
460 DiagKind::NotAContainer {
461 name: x,
462 found: DefKind::Fn,
463 },
464 DiagKind::Shadowing {
465 name: x,
466 shadowed: b,
467 },
468 DiagKind::BrokenImport { name: x },
469 DiagKind::ImportCycle { name: x },
470 DiagKind::ImportAmbiguity { name: x },
471 DiagKind::GlobsUnsupported,
472 DiagKind::AliasesUnsupported,
473 DiagKind::RootUnsupported {
474 root: PathRoot::StaticType,
475 },
476 DiagKind::OutsideType {
477 root: PathRoot::SelfType,
478 },
479 DiagKind::NoParent,
480 DiagKind::SuperBeyondRoot { levels: 2 },
481 DiagKind::MixinConflict {
482 name: x,
483 first: d,
484 second: d,
485 },
486 DiagKind::NotAMixin {
487 found: DefKind::Sum,
488 },
489 DiagKind::UnknownMixinMember { name: x.sym },
490 DiagKind::MixinCycle,
491 DiagKind::Rejected {
492 error: HirError::RootNotModule,
493 },
494 ];
495 for k in kinds {
496 let m = diag(k).message(&names);
497 assert!(!m.is_empty());
498 assert!(!m.contains('?') || k.is_unresolved(), "{m}");
499 }
500 assert_eq!(diag(kinds[0]).message(&names), "cannot find `x` in `x`");
501 assert_eq!(diag(kinds[2]).message(&names), "`x` is protected");
502 }
503
504 #[test]
505 fn test_message_survives_unknown_symbol() {
506 let names = Interner::new();
507 let ghost = Name::new(hir_lang::Symbol::from_u32(77).unwrap());
508 let m = diag(DiagKind::BrokenImport { name: ghost }).message(&names);
509 assert!(m.contains("`?`"));
510 }
511}