use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
use diagnostics::UnusedExpressionKind;
use syntax::ast::{Expression, Generic, Pattern, Span, StructKind, UnaryOperator};
use syntax::program::{Definition, ReceiverCoercion};
use syntax::types::{Bound, Type, TypeVariableState};
use crate::checker::PostInferenceCheck;
use super::super::Checker;
use super::expressions::patterns::collect_pattern_bindings;
impl Checker<'_, '_> {
pub(crate) fn check_unused_expression(
&mut self,
span: Span,
ty: &Type,
is_literal: bool,
allowed_lints: &[String],
) {
let kind = if is_literal {
Some(UnusedExpressionKind::Literal)
} else if ty.is_result() {
Some(UnusedExpressionKind::Result)
} else if ty.is_option() {
Some(UnusedExpressionKind::Option)
} else if ty.is_partial() {
Some(UnusedExpressionKind::Partial)
} else if !ty.is_unit() && !ty.is_variable() && !ty.is_never() {
Some(UnusedExpressionKind::Value)
} else {
None
};
if let Some(kind) = kind
&& !allowed_lints.contains(&kind.lint_name().to_string())
{
self.facts.add_unused_expression(span, kind);
}
}
pub(crate) fn callee_allowed_lints(
&mut self,
callee_name: &str,
inferred: &Expression,
) -> Vec<String> {
if let Some(qualified) = self.lookup_qualified_name(callee_name)
&& let Some(definition) = self.store.get_definition(&qualified)
{
return definition.allowed_lints().to_vec();
}
if let Expression::Call { expression, .. } = inferred
&& let Expression::DotAccess {
expression: receiver,
member,
..
} = expression.as_ref()
{
let receiver_ty = receiver.get_type().resolve().strip_refs();
if let Type::Constructor { id, .. } = &receiver_ty {
let method_key = format!("{}.{}", id, member);
if let Some(definition) = self.store.get_definition(&method_key) {
return definition.allowed_lints().to_vec();
}
}
}
vec![]
}
pub(crate) fn get_call_return_type(expression: &Expression) -> Option<Type> {
if let Expression::Call { expression, .. } = expression {
let callee_ty = expression.get_type().resolve();
match callee_ty {
Type::Function { return_type, .. } => Some(return_type.as_ref().clone()),
_ => None,
}
} else {
None
}
}
pub(crate) fn is_channel_send(expression: &Expression) -> bool {
if let Expression::Call { expression, .. } = expression
&& let Expression::DotAccess {
expression: receiver,
member,
..
} = expression.as_ref()
&& member == "send"
{
return receiver
.get_type()
.resolve()
.get_name()
.map(|n| n == "Channel" || n == "Sender")
.unwrap_or(false);
}
false
}
pub(crate) fn check_unused_type_parameters(&mut self, generics: &[Generic], fn_ty: &Type) {
if generics.is_empty() {
return;
}
let mut remaining: HashSet<_> = generics.iter().map(|g| g.name.clone()).collect();
fn_ty.remove_found_type_names(&mut remaining);
let is_typedef = self.is_d_lis();
for generic in generics {
if generic.name.starts_with('_') {
continue;
}
if remaining.contains(&generic.name) {
self.facts.add_unused_type_param(
generic.name.to_string(),
generic.span,
is_typedef,
);
}
}
}
pub(crate) fn check_prelude_shadowing(&mut self, name: &str, span: Span) {
if self.is_d_lis() {
return;
}
let prelude_qualified_name = format!("prelude.{}", name);
if let Some(prelude_module) = self.store.get_module("prelude")
&& prelude_module
.definitions
.contains_key(prelude_qualified_name.as_str())
{
self.sink
.push(diagnostics::infer::prelude_type_shadowed(name, span));
}
}
pub(crate) fn check_slice_index_type(
&mut self,
type_name: &str,
index_ty: &Type,
span: Span,
) -> bool {
if type_name != "Slice" || index_ty.is_variable() {
return true;
}
if index_ty.get_name().is_some_and(|n| n == "int") {
return true;
}
self.sink
.push(diagnostics::infer::slice_index_type_mismatch(
index_ty, span,
));
false
}
pub(crate) fn check_call_arity(
&mut self,
param_types: &[Type],
args: &[Expression],
callee_expression: &Expression,
span: &Span,
) {
if param_types.len() != args.len() {
let actual_types: Vec<Type> = args.iter().map(|e| e.get_type()).collect();
let generic_params = self.get_generic_param_names(callee_expression);
let is_constructor = callee_expression
.get_var_name()
.map(|name| name.chars().next().is_some_and(|c| c.is_uppercase()))
.unwrap_or(false);
self.sink.push(diagnostics::infer::arity_mismatch(
param_types,
&actual_types,
&generic_params,
is_constructor,
*span,
));
}
}
fn get_generic_param_names(&self, expression: &Expression) -> Vec<String> {
if let Expression::Identifier { value, .. } = expression
&& let Some(ty) = self.scopes.lookup_value(value)
{
return match ty {
Type::Forall { vars, .. } => vars.iter().map(|s| s.to_string()).collect(),
_ => vec![],
};
}
vec![]
}
pub(crate) fn check_unconstrained_bounded_type_params(
&mut self,
bounds: &[Bound],
span: &Span,
) {
for bound in bounds {
if let Type::Variable(var) = &bound.generic.resolve()
&& let TypeVariableState::Unbound { .. } = &*var.borrow()
{
self.sink.push(diagnostics::infer::unconstrained_type_param(
&bound.param_name,
*span,
));
}
}
}
pub fn run_post_inference_checks(&mut self) {
for check in std::mem::take(&mut self.post_inference_checks) {
match check {
PostInferenceCheck::GenericCall { return_ty, span } => {
if return_ty.resolve().has_unbound_variables() {
self.sink
.push(diagnostics::infer::cannot_infer_type_argument(span));
}
}
PostInferenceCheck::EmptyCollection { name, ty, span } => {
if ty.resolve().has_unbound_variables() {
self.sink
.push(diagnostics::infer::uninferred_binding(&name, span));
}
}
PostInferenceCheck::StatementTail { expected_ty, span } => {
let resolved = expected_ty.resolve();
if !resolved.is_unit()
&& !matches!(resolved, Type::Variable(_))
&& !expected_ty.is_ignored()
{
self.sink.push(diagnostics::infer::statement_as_tail(span));
}
}
}
}
}
pub(crate) fn check_constrained_return_type(
&mut self,
return_ty: &Type,
generics: &[Generic],
span: &Span,
fn_name: &str,
) {
let Type::Constructor { id, params, .. } = return_ty else {
return;
};
if params.is_empty() {
return;
}
let qualified_id = self
.lookup_qualified_name(id)
.unwrap_or_else(|| id.as_ref().to_string());
let Some(definition) = self.store.get_definition(&qualified_id) else {
return;
};
let methods = match definition {
syntax::program::Definition::Struct { methods, .. } => methods,
syntax::program::Definition::Enum { methods, .. } => methods,
_ => return,
};
let mut required_bounds: HashMap<String, Vec<Type>> = HashMap::default();
for method_ty in methods.values() {
if let Type::Forall { vars: _, body } = method_ty {
if let Type::Function { bounds, .. } = body.as_ref() {
for bound in bounds {
if let Type::Parameter(param_name) = &bound.generic {
let entry = required_bounds
.entry(param_name.as_ref().to_string())
.or_default();
if !entry.contains(&bound.ty) {
entry.push(bound.ty.clone());
}
}
}
}
} else if let Type::Function { bounds, .. } = method_ty {
for bound in bounds {
if let Type::Parameter(param_name) = &bound.generic {
let entry = required_bounds
.entry(param_name.as_ref().to_string())
.or_default();
if !entry.contains(&bound.ty) {
entry.push(bound.ty.clone());
}
}
}
}
}
for return_param in params.iter() {
if let Type::Parameter(param_name) = return_param
&& let Some(method_bounds) = required_bounds.get(param_name.as_ref())
{
let fn_generic = generics
.iter()
.find(|g| g.name.as_ref() == param_name.as_ref());
if let Some(fn_gen) = fn_generic {
let fn_bounds: Vec<Type> = fn_gen
.bounds
.iter()
.map(|b| self.convert_to_type(b, span))
.collect();
for method_bound in method_bounds {
if !fn_bounds.iter().any(|fb| fb == method_bound) {
self.sink.push(
diagnostics::infer::missing_constraint_on_generic_return_type(
fn_name,
param_name.as_ref(),
method_bound,
*span,
),
);
}
}
} else {
let scope_bounds = self.scopes.collect_all_trait_bounds();
let qualified = self.qualify_name(param_name.as_ref());
let impl_bounds = scope_bounds
.get(qualified.as_str())
.or_else(|| scope_bounds.get(param_name.as_ref()));
let all_covered = impl_bounds
.is_some_and(|ib| method_bounds.iter().all(|mb| ib.contains(mb)));
if !all_covered {
let bound_str = method_bounds
.iter()
.map(|b| b.to_string())
.collect::<Vec<_>>()
.join(" + ");
self.sink.push(
diagnostics::infer::missing_constraint_on_generic_return_type(
fn_name,
param_name.as_ref(),
&Type::Parameter(bound_str.into()),
*span,
),
);
}
}
}
}
}
pub(crate) fn check_newtype_field_assignment(&mut self, target: &Expression, span: Span) {
match target {
Expression::DotAccess {
expression, member, ..
} => {
if member == "0" {
let base_ty = expression.get_type().resolve();
let ty = base_ty.strip_refs();
if let Type::Constructor { id, params, .. } = ty
&& params.is_empty()
&& let Some(Definition::Struct {
kind: StructKind::Tuple,
fields,
generics,
..
}) = self.store.get_definition(id.as_str())
&& fields.len() == 1
&& generics.is_empty()
{
let type_name = id.rsplit('.').next().unwrap_or(id.as_str());
self.sink.push(diagnostics::infer::newtype_field_assignment(
type_name, span,
));
return;
}
}
self.check_newtype_field_assignment(expression, span);
}
Expression::IndexedAccess { expression, .. } => {
self.check_newtype_field_assignment(expression, span);
}
Expression::Unary {
operator: UnaryOperator::Deref,
expression,
..
} => {
self.check_newtype_field_assignment(expression, span);
}
_ => {}
}
}
pub(crate) fn check_map_field_chain_assignment(&mut self, target: &Expression, span: Span) {
if self.has_map_field_in_chain(target) {
self.sink
.push(diagnostics::infer::map_field_chain_assignment(span));
}
}
pub(crate) fn has_map_field_in_chain(&self, expression: &Expression) -> bool {
match expression.unwrap_parens() {
Expression::DotAccess { expression, .. } => {
self.is_map_indexed_access(expression) || self.has_map_field_in_chain(expression)
}
_ => false,
}
}
fn is_map_indexed_access(&self, expression: &Expression) -> bool {
match expression.unwrap_parens() {
Expression::IndexedAccess { expression, .. } => {
expression.get_type().resolve().has_name("Map")
}
_ => false,
}
}
pub(crate) fn check_native_value_usage(&mut self, value: &str, ty: &Type, span: Span) {
if matches!(
value,
"imaginary" | "assert_type" | "complex" | "real" | "panic"
) {
let qualified = format!("{}.{}", self.cursor.module_id, value);
if self.store.get_definition(&qualified).is_none() {
self.sink
.push(diagnostics::infer::native_constructor_value(value, span));
return;
}
}
{
let qualified = if value.contains('.') {
value.to_string()
} else {
format!("{}.{}", self.cursor.module_id, value)
};
let is_tuple_struct = match self.store.get_definition(&qualified) {
Some(Definition::Struct {
kind: StructKind::Tuple,
..
}) => true,
Some(Definition::TypeAlias { ty: alias_ty, .. }) => {
if let Type::Constructor { id, .. } = alias_ty.unwrap_forall() {
matches!(
self.store.get_definition(id),
Some(Definition::Struct {
kind: StructKind::Tuple,
..
})
)
} else {
false
}
}
_ => false,
};
if is_tuple_struct {
self.sink
.push(diagnostics::infer::native_constructor_value(value, span));
return;
}
}
let Some((type_part, method_part)) = value.split_once('.') else {
return;
};
if method_part.contains('.') {
return;
}
let is_native = matches!(
type_part,
"Slice" | "EnumeratedSlice" | "Map" | "Channel" | "Sender" | "Receiver" | "string"
);
if is_native {
if matches!(method_part, "new" | "buffered") {
self.sink
.push(diagnostics::infer::native_constructor_value(value, span));
} else {
self.sink
.push(diagnostics::infer::native_method_value(method_part, span));
}
return;
}
if matches!(method_part, "new" | "buffered") {
let ret_ty = match ty {
Type::Function { return_type, .. } => Some(return_type.as_ref()),
Type::Forall { body, .. } => match body.as_ref() {
Type::Function { return_type, .. } => Some(return_type.as_ref()),
_ => None,
},
_ => None,
};
if let Some(ret) = ret_ty {
let resolved = ret.resolve();
let is_native_ret =
matches!(resolved.get_name(), Some("Channel" | "Map" | "Slice"));
if is_native_ret {
self.sink
.push(diagnostics::infer::native_constructor_value(value, span));
return;
}
}
}
let is_fn = matches!(ty, Type::Function { .. } | Type::Forall { .. });
if !is_fn {
return;
}
let fn_params = match ty {
Type::Function { params, .. } => params.as_slice(),
Type::Forall { body, .. } => match body.as_ref() {
Type::Function { params, .. } => params.as_slice(),
_ => return,
},
_ => return,
};
let Some(first) = fn_params.first() else {
return;
};
let stripped = first.strip_refs();
let is_self = matches!(&stripped, Type::Constructor { id, .. }
if id.rsplit('.').next() == Some(type_part));
if !is_self {
return;
}
let module_id = &self.cursor.module_id;
let method_key = format!("{}.{}.{}", module_id, type_part, method_part);
let is_public = self
.store
.get_definition(&method_key)
.map(|d| d.visibility().is_public())
.unwrap_or(true);
if !is_public {
self.sink
.push(diagnostics::infer::private_method_expression(span));
}
}
pub(crate) fn has_newtype_dot0_in_chain(&self, expression: &Expression) -> bool {
let mut current = expression.unwrap_parens();
while let Expression::DotAccess {
expression: inner,
member,
..
} = current
{
if member.parse::<usize>().is_ok() {
let ty = inner.get_type().resolve().strip_refs();
if let Type::Constructor { id, .. } = &ty
&& let Some(Definition::Struct {
kind: StructKind::Tuple,
fields,
generics,
..
}) = self.store.get_definition(id.as_str())
&& fields.len() == 1
&& generics.is_empty()
{
return true;
}
}
current = inner.unwrap_parens();
}
false
}
pub(crate) fn check_not_temp_producing(&mut self, expression: &Expression) {
if is_temp_producing(expression) || self.has_auto_address_on_call(expression) {
self.sink.push(diagnostics::infer::complex_sub_expression(
expression.get_span(),
));
}
}
fn has_auto_address_on_call(&self, expression: &Expression) -> bool {
let expression = expression.unwrap_parens();
if let Expression::Call { expression, .. } = expression
&& let Expression::DotAccess {
expression: receiver,
..
} = expression.unwrap_parens()
{
if matches!(receiver.unwrap_parens(), Expression::Call { .. })
&& self.coercions.get_coercion(receiver.get_span())
== Some(ReceiverCoercion::AutoAddress)
{
return true;
}
return self.has_auto_address_on_call(receiver);
}
false
}
}
pub(crate) fn is_temp_producing(expression: &Expression) -> bool {
matches!(
expression.unwrap_parens(),
Expression::If { .. }
| Expression::IfLet { .. }
| Expression::Match { .. }
| Expression::Block { .. }
| Expression::Loop { .. }
| Expression::Select { .. }
| Expression::TryBlock { .. }
| Expression::RecoverBlock { .. }
)
}
pub(crate) fn check_is_non_addressable(expression: &Expression) -> Option<&'static str> {
match expression {
Expression::Identifier { .. } => None,
Expression::DotAccess { expression, .. } => {
let inner = expression.unwrap_parens();
let is_non_addressable_origin = matches!(inner, Expression::StructCall { .. })
|| (matches!(inner, Expression::Call { .. })
&& !expression.get_type().resolve().is_ref());
if is_non_addressable_origin {
Some("field access on non-addressable value")
} else {
check_is_non_addressable(expression)
}
}
Expression::IndexedAccess { expression, .. } => {
let expression_ty = expression.get_type().resolve();
if let Some(name) = expression_ty.get_name() {
if name == "Map" {
return Some("map index expression");
}
if name == "Slice" {
return None;
}
}
if matches!(expression.unwrap_parens(), Expression::Call { .. }) {
Some("index access on function call")
} else {
check_is_non_addressable(expression)
}
}
Expression::Unary {
operator: UnaryOperator::Deref,
..
} => None,
Expression::StructCall { .. } => None,
Expression::Paren { expression, .. } => check_is_non_addressable(expression),
Expression::Call { .. } => None,
Expression::Literal { .. } => Some("literal"),
Expression::Binary { .. } => Some("binary expression"),
Expression::If { .. } | Expression::IfLet { .. } => Some("conditional expression"),
Expression::Match { .. } => Some("match expression"),
Expression::Block { .. } => Some("block expression"),
Expression::Lambda { .. } => Some("lambda"),
Expression::Tuple { .. } => Some("tuple"),
Expression::Range { .. } => Some("range expression"),
_ => Some("expression"),
}
}
pub(crate) fn check_non_addressable_assignment_target(
expression: &Expression,
) -> Option<&'static str> {
match expression.unwrap_parens() {
Expression::Identifier { .. } => None,
Expression::DotAccess { expression, .. } => {
if matches!(expression.unwrap_parens(), Expression::Call { .. })
&& expression.get_type().resolve().is_ref()
{
None
} else {
check_non_addressable_assignment_target(expression)
}
}
Expression::IndexedAccess { .. } => None,
Expression::Unary {
operator: UnaryOperator::Deref,
..
} => None,
Expression::Call { .. } => Some("function call result"),
Expression::StructCall { .. } => Some("struct literal"),
Expression::Tuple { .. } => Some("tuple literal"),
_ => None,
}
}
pub(crate) fn check_duplicate_bindings(sink: &diagnostics::DiagnosticSink, pattern: &Pattern) {
if let Pattern::Or { patterns, .. } = pattern {
for alternative_pattern in patterns {
check_duplicate_bindings(sink, alternative_pattern);
}
return;
}
if matches!(
pattern,
Pattern::Identifier { .. }
| Pattern::WildCard { .. }
| Pattern::Literal { .. }
| Pattern::Unit { .. }
) {
return;
}
let bindings = collect_pattern_bindings(pattern);
let mut seen: HashMap<&str, &Span> = HashMap::default();
for (name, span) in &bindings {
if let Some(first_span) = seen.get(name.as_str()) {
sink.push(diagnostics::infer::duplicate_binding_in_pattern(
name,
*(*first_span),
*span,
));
} else {
seen.insert(name, span);
}
}
}
pub(crate) fn check_binding_pattern(sink: &diagnostics::DiagnosticSink, pattern: &Pattern) {
if matches!(pattern, Pattern::Literal { .. }) {
sink.push(diagnostics::infer::literal_pattern_in_binding(
pattern.get_span(),
));
}
if matches!(pattern, Pattern::Or { .. }) {
sink.push(diagnostics::infer::or_pattern_in_irrefutable_context(
pattern.get_span(),
));
}
}
pub(crate) fn check_receiver(
sink: &diagnostics::DiagnosticSink,
method: &Expression,
impl_ty: &Type,
) {
let Expression::Function { params, .. } = method else {
return;
};
let Some(first_param) = params.first() else {
return;
};
let Pattern::Identifier { identifier, span } = &first_param.pattern else {
return;
};
let receiver_ty = first_param.ty.strip_refs();
let types_match = receiver_ty == *impl_ty;
if types_match && identifier != "self" {
sink.push(diagnostics::infer::receiver_must_be_named_self(
identifier, *span,
));
}
if !types_match && identifier == "self" {
let annotation_span = first_param
.annotation
.as_ref()
.map(|a| a.get_span())
.unwrap_or_else(|| *span);
let impl_type_name = impl_ty.get_name().unwrap_or_default();
let receiver_type_name = receiver_ty.get_name().unwrap_or_default();
sink.push(diagnostics::infer::receiver_type_mismatch(
impl_type_name,
receiver_type_name,
annotation_span,
));
}
}
pub fn check_interface_visibility(
store: &crate::store::Store,
module_id: &str,
sink: &diagnostics::DiagnosticSink,
) {
let module = match store.get_module(module_id) {
Some(m) => m,
None => return,
};
let non_pub_interfaces: HashMap<String, HashSet<String>> = module
.definitions
.iter()
.filter(|(key, _)| key.starts_with(&format!("{}.", module_id)))
.filter_map(|(_, definition)| {
if let syntax::program::Definition::Interface {
visibility: syntax::program::Visibility::Private,
definition: interface_data,
..
} = definition
{
let method_names = interface_data
.methods
.keys()
.map(|k| k.to_string())
.collect();
Some((interface_data.name.to_string(), method_names))
} else {
None
}
})
.collect();
if non_pub_interfaces.is_empty() {
return;
}
for (_, definition) in module
.definitions
.iter()
.filter(|(key, _)| key.starts_with(&format!("{}.", module_id)))
{
if let syntax::program::Definition::Struct {
methods,
name,
name_span,
..
} = definition
{
for method_name in methods.keys() {
for (interface_name, interface_methods) in &non_pub_interfaces {
if interface_methods.contains(method_name.as_str()) {
let method_key = format!("{}.{}.{}", module_id, name, method_name);
let method_is_pub = module
.definitions
.get(method_key.as_str())
.map(|definition| definition.visibility().is_public())
.unwrap_or(false);
if method_is_pub {
sink.push(diagnostics::infer::non_pub_interface_with_pub_impl(
interface_name,
name,
*name_span,
));
return;
}
}
}
}
}
}
}