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}