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}