Skip to main content

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