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    /// 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}