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