use nova_ast as ast;
use nova_ast::item::ImportKind;
use nova_diagnostics::{lines, suggest, Edit, FileId, Fix, Span, Spanned};
use nova_hir::{Ty, TyHead};
use nova_resolver::{DefId, DefKind, Exported, ModuleId, Res, ScopeEntry};
use super::{Checker, FnCtx, MethodRes};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum Binding {
Let,
Param,
Other,
}
#[derive(Clone, Debug)]
pub(super) enum Need {
Value,
Call(usize),
Type,
Record,
Trait,
Qualifier { member: String, call: Option<usize> },
Pattern {
variant: String,
arity: usize,
scrutinee: Option<DefId>,
},
}
const PRIMITIVES: [&str; 7] = ["Bool", "Bytes", "Char", "Float", "Future", "Int", "String"];
pub(super) fn visible_locals(fcx: &FnCtx) -> Vec<String> {
let mut names: Vec<String> = fcx
.scopes
.iter()
.flat_map(|scope| scope.keys().cloned())
.filter(|n| !n.starts_with("__"))
.collect();
names.sort();
names.dedup();
names
}
pub(super) fn scrutinee_sum(fcx: &FnCtx, ty: &Ty) -> Option<DefId> {
match fcx.icx.apply(ty) {
Ty::Sum { def_id, .. } => Some(def_id),
_ => None,
}
}
impl<'a> Checker<'a> {
pub(super) fn mutable_fix(&self, name: &str, decl: Span) -> Option<Fix> {
if name == "self" || !self.defs.owned(self.cur_module) {
return None;
}
match self.bindings.get(&decl) {
Some(Binding::Let | Binding::Param) => Some(Fix::new(
format!("make `{name}` mutable"),
vec![Edit::insert(decl.start, decl.file, "mut ")],
)),
_ => None,
}
}
pub(super) fn push_fix(&mut self, fix: Option<Fix>) {
if !self.defs.owned(self.cur_module) {
return;
}
if let (Some(fix), Some(d)) = (fix, self.diagnostics.last_mut()) {
d.fixes.push(fix);
}
}
pub(super) fn name_fixes(&mut self, name: Spanned<String>, need: Need, nearby: Vec<String>) {
let import = self.import_fix(&name, &need);
let change = self.did_you_mean(&name, &need, &nearby);
self.push_fix(import);
self.push_fix(change);
}
fn import_fix(&self, name: &Spanned<String>, need: &Need) -> Option<Fix> {
let x = name.value.as_str();
let here = self.cur_module;
let found: Vec<(&str, ModuleId)> = self
.defs
.importable(here)
.iter()
.filter(|(_, m)| self.fits(&self.defs.exported(*m, x), need))
.map(|(import, m)| (import.as_str(), *m))
.collect();
let [(import, module)] = found.as_slice() else {
return None;
};
let exported = self.defs.exported(*module, x);
let bound = self.defs.bound_outside_std(here, x);
let clash = (exported.value.is_some() && bound.value.is_some())
|| (exported.ty.is_some() && bound.ty.is_some())
|| (exported.trait_def.is_some() && bound.trait_def.is_some());
if clash {
return None;
}
let edit = self.import_edit(name.span.file, import, x)?;
Some(Fix::new(
format!("import `{x}` from `{import}`"),
vec![edit],
))
}
fn import_edit(&self, file: FileId, import: &str, x: &str) -> Option<Edit> {
let here = self.cur_module;
let items: Vec<&ast::Item> = self
.file
.items
.iter()
.enumerate()
.filter(|(i, _)| self.defs.module_of(*i) == here)
.map(|(_, item)| &item.value)
.collect();
let spans: Vec<Span> = self
.file
.items
.iter()
.enumerate()
.filter(|(i, _)| self.defs.module_of(*i) == here)
.map(|(_, item)| item.span)
.collect();
let list = items.iter().rev().find_map(|item| match item {
ast::Item::Import(imp) => match (&imp.kind, imp.path.value.segments.as_slice()) {
(ImportKind::List(names), [seg]) if seg.value == import => names.last(),
_ => None,
},
_ => None,
});
if let Some(last) = list {
return Some(Edit::insert(last.span.end, file, format!(", {x}")));
}
let text = self.sources?.get_source(file)?;
let line = format!("import {import}::{{{x}}}");
let last_import = items
.iter()
.zip(&spans)
.filter(|(item, _)| matches!(item, ast::Item::Import(_)))
.map(|(_, span)| *span)
.next_back();
let (at, inserted) = match (last_import, spans.first()) {
(Some(last), _) => lines::line_after(text, last.end as usize, &line),
(None, Some(first)) => lines::line_before_item(text, first.start as usize, &line),
(None, None) => (text.len(), format!("{line}{}", lines::ending(text))),
};
Some(Edit::insert(at as u32, file, inserted))
}
fn fits(&self, exported: &Exported, need: &Need) -> bool {
let kind = |id: DefId| &self.defs.def(id).kind;
match need {
Need::Value => match exported.value {
Some(Res::Def(id)) => {
matches!(kind(id), DefKind::Fn { .. } | DefKind::Const { .. })
}
Some(Res::Variant(sum, vi)) => self.variant_arity(sum, vi) == Some(0),
_ => false,
},
Need::Call(n) => match exported.value {
Some(Res::Def(id)) => self.arity(id) == Some(*n),
Some(Res::Variant(sum, vi)) => self.variant_arity(sum, vi) == Some(*n),
_ => false,
},
Need::Type => exported
.ty
.is_some_and(|id| matches!(kind(id), DefKind::Record { .. } | DefKind::Sum { .. })),
Need::Record => exported
.ty
.is_some_and(|id| matches!(kind(id), DefKind::Record { .. })),
Need::Trait => exported.trait_def.is_some(),
Need::Qualifier { member, call } => exported
.ty
.is_some_and(|id| self.has_member(id, member, *call)),
Need::Pattern {
variant,
arity,
scrutinee,
} => exported.ty.is_some_and(|id| {
scrutinee.map_or(true, |s| s == id)
&& self
.variant_named(id, variant)
.is_some_and(|(_, a)| a == *arity)
}),
}
}
fn arity(&self, id: DefId) -> Option<usize> {
match self.defs.def(id).kind {
DefKind::Fn { .. } => self.sigs.get(&id).map(|s| s.params.len()),
DefKind::ExternFn { .. } => self
.externs
.iter()
.find(|e| e.def_id == id)
.map(|e| e.params.len()),
_ => None,
}
}
fn variant_arity(&self, sum: DefId, vi: usize) -> Option<usize> {
match &self.defs.def(sum).kind {
DefKind::Sum { variants, .. } => variants.get(vi).map(|v| v.arity),
_ => None,
}
}
fn variant_named(&self, sum: DefId, name: &str) -> Option<(usize, usize)> {
match &self.defs.def(sum).kind {
DefKind::Sum { variants, .. } => variants
.iter()
.position(|v| v.name == name)
.map(|i| (i, variants[i].arity)),
_ => None,
}
}
fn has_member(&self, id: DefId, member: &str, call: Option<usize>) -> bool {
let head = match self.defs.def(id).kind {
DefKind::Record { .. } => TyHead::Record(id),
DefKind::Sum { .. } => TyHead::Sum(id),
_ => return false,
};
if let Some((_, arity)) = self.variant_named(id, member) {
return arity == call.unwrap_or(0);
}
let Some(n) = call else {
return false;
};
matches!(
self.find_assoc_fns(head, member).as_slice(),
[f] if self.sigs.get(f).is_some_and(|s| s.params.len() == n)
)
}
fn did_you_mean(&self, name: &Spanned<String>, need: &Need, nearby: &[String]) -> Option<Fix> {
let mut candidates: Vec<String> = nearby.to_vec();
for (n, entry) in self.defs.names_in_scope(self.cur_module) {
if self.offers(need, entry) {
candidates.push(n);
}
}
if matches!(need, Need::Type | Need::Qualifier { .. }) {
candidates.extend(PRIMITIVES.iter().map(|p| p.to_string()));
}
suggest::change_to(name, candidates.iter().map(String::as_str))
}
fn offers(&self, need: &Need, entry: ScopeEntry) -> bool {
let kind = |id: DefId| &self.defs.def(id).kind;
match (need, entry) {
(Need::Value, ScopeEntry::Value(Res::Def(id))) => {
matches!(kind(id), DefKind::Fn { .. } | DefKind::Const { .. })
}
(Need::Value, ScopeEntry::Value(Res::Variant(sum, vi))) => {
self.variant_arity(sum, vi) == Some(0)
}
(Need::Call(_), ScopeEntry::Value(Res::Def(id))) => {
matches!(kind(id), DefKind::Fn { .. } | DefKind::ExternFn { .. })
}
(Need::Call(_), ScopeEntry::Value(Res::Variant(..) | Res::Builtin(_))) => true,
(Need::Type | Need::Qualifier { .. }, ScopeEntry::Type(_)) => true,
(Need::Record, ScopeEntry::Type(id)) => matches!(kind(id), DefKind::Record { .. }),
(Need::Pattern { .. }, ScopeEntry::Type(id)) => matches!(kind(id), DefKind::Sum { .. }),
(Need::Trait, ScopeEntry::Trait(_)) => true,
_ => false,
}
}
pub(super) fn field_fix(&self, ty: &Ty, field: &Spanned<String>) -> Option<Fix> {
let Ty::Record { def_id, .. } = ty else {
return None;
};
let record = self.records.iter().find(|r| r.def_id == *def_id)?;
suggest::change_to(field, record.fields.iter().map(|f| f.name.as_str()))
}
pub(super) fn method_fix(
&self,
recv: &Ty,
fcx: &FnCtx,
method: &Spanned<String>,
) -> Option<Fix> {
let trait_methods = |tid: DefId| {
self.traits
.iter()
.filter(move |t| t.def_id == tid)
.flat_map(|t| t.methods.iter().map(|m| m.name.clone()))
};
let mut names: Vec<String> = Vec::new();
match recv {
Ty::Param(k) => {
for &tid in fcx.param_bounds.get(*k as usize).into_iter().flatten() {
names.extend(trait_methods(tid));
}
}
_ => {
if let Some(head) = recv.head() {
for imp in self.impls.iter().filter(|i| i.self_head == head) {
names.extend(imp.methods.iter().map(|(n, _)| n.clone()));
if let Some(tid) = imp.trait_id {
names.extend(trait_methods(tid));
}
}
}
}
}
names.sort();
names.dedup();
names.retain(|n| {
matches!(
self.resolve_method_on(recv, fcx, n),
MethodRes::Inherent(_) | MethodRes::Trait(..)
)
});
suggest::change_to(method, names.iter().map(String::as_str))
}
pub(super) fn remove_arm_fix(&self, arm: Span) -> Option<Fix> {
if !self.defs.owned(self.cur_module) {
return None;
}
let text = self.sources?.get_source(arm.file)?;
let (start, end) = lines::arm_removal(text, arm.start as usize, arm.end as usize)?;
Some(Fix::new(
"remove the unreachable arm",
vec![Edit::replace(
Span::new(start as u32, end as u32, arm.file),
"",
)],
))
}
}