use diagnostics::UnusedExpressionKind;
use syntax::ast::{Annotation, Expression, SelectArmPattern, Span, UnaryOperator};
use syntax::types::{Symbol, Type};
use crate::facts::LocalFacts;
use crate::store::Store;
use diagnostics::infer::MismatchedTailKind;
struct TailContext<'a> {
expected_span: Span,
expected_ty: &'a Type,
}
pub(crate) fn run(
typed_ast: &[Expression],
module_id: &str,
store: &Store,
facts: &mut LocalFacts,
) {
for item in typed_ast {
visit_expression(item, None, module_id, store, facts);
}
}
fn visit_expression(
expression: &Expression,
tail_ctx: Option<&TailContext<'_>>,
module_id: &str,
store: &Store,
facts: &mut LocalFacts,
) {
match expression {
Expression::Block { items, ty, .. }
| Expression::TryBlock { items, ty, .. }
| Expression::RecoverBlock { items, ty, .. } => {
let tail_is_discarded =
tail_ctx.is_some() || ty.is_unit() || ty.is_ignored() || ty.is_never();
visit_block_items(items, tail_is_discarded, tail_ctx, module_id, store, facts);
}
Expression::Function {
body,
return_type,
return_annotation,
..
} => {
let is_implicit_return = matches!(return_annotation, Annotation::Unknown);
let body_ty = body.get_type();
let tail_is_discarded = is_implicit_return
&& (return_type.is_unit() || body_ty.is_ignored() || body_ty.is_never());
let ctx = tail_is_discarded.then(|| TailContext {
expected_span: signature_marker_span(body.get_span()),
expected_ty: return_type,
});
visit_expression(body, ctx.as_ref(), module_id, store, facts);
return;
}
Expression::Lambda {
body,
ty,
span,
return_annotation,
..
} => {
let is_implicit_return = matches!(return_annotation, Annotation::Unknown);
let lambda_returns_unit =
matches!(ty, Type::Function { return_type, .. } if return_type.is_unit());
let body_ty = body.get_type();
let tail_is_discarded = is_implicit_return
&& (lambda_returns_unit
|| body_ty.is_unit()
|| body_ty.is_ignored()
|| body_ty.is_never());
let lambda_return_ty: &Type = match ty {
Type::Function { return_type, .. } => return_type,
_ => &body_ty,
};
let ctx = tail_is_discarded.then(|| TailContext {
expected_span: signature_marker_span(*span),
expected_ty: lambda_return_ty,
});
if tail_is_discarded && !matches!(body.as_ref(), Expression::Block { .. }) {
descend_discarded(
body,
&DiscardMode::Tail(ctx.as_ref()),
module_id,
store,
facts,
);
}
visit_expression(body, ctx.as_ref(), module_id, store, facts);
return;
}
_ => {}
}
for child in expression.children() {
visit_expression(child, None, module_id, store, facts);
}
}
fn signature_marker_span(span: Span) -> Span {
Span::new(span.file_id, span.byte_offset, 1)
}
fn visit_block_items(
items: &[Expression],
tail_is_discarded: bool,
tail_ctx: Option<&TailContext<'_>>,
module_id: &str,
store: &Store,
facts: &mut LocalFacts,
) {
let len = items.len();
for (i, item) in items.iter().enumerate() {
let is_last = i == len - 1;
let is_statement_only = is_statement_only(item);
if !is_statement_only && !is_last {
descend_discarded(item, &DiscardMode::NonTail, module_id, store, facts);
}
if is_last && !is_statement_only && tail_is_discarded {
descend_discarded(item, &DiscardMode::Tail(tail_ctx), module_id, store, facts);
}
}
}
enum DiscardMode<'a> {
Tail(Option<&'a TailContext<'a>>),
NonTail,
}
fn descend_discarded(
expression: &Expression,
mode: &DiscardMode<'_>,
module_id: &str,
store: &Store,
facts: &mut LocalFacts,
) {
match expression.unwrap_parens() {
Expression::Block { items, .. }
| Expression::TryBlock { items, .. }
| Expression::RecoverBlock { items, .. } => {
if let Some(last) = items.last()
&& !is_statement_only(last)
{
descend_discarded(last, mode, module_id, store, facts);
}
}
Expression::If {
consequence,
alternative,
..
} => {
descend_discarded(consequence, mode, module_id, store, facts);
descend_discarded(alternative, mode, module_id, store, facts);
}
Expression::Match { arms, .. } => {
for arm in arms {
descend_discarded(&arm.expression, mode, module_id, store, facts);
}
}
Expression::Select { arms, .. } => {
for arm in arms {
match &arm.pattern {
SelectArmPattern::Receive { body, .. }
| SelectArmPattern::Send { body, .. }
| SelectArmPattern::WildCard { body } => {
descend_discarded(body, mode, module_id, store, facts);
}
SelectArmPattern::MatchReceive {
arms: match_arms, ..
} => {
for match_arm in match_arms {
descend_discarded(&match_arm.expression, mode, module_id, store, facts);
}
}
}
}
}
Expression::Loop { body, .. } => {
descend_loop_break_values(body, mode, module_id, store, facts);
}
Expression::Let { .. }
| Expression::Const { .. }
| Expression::Assignment { .. }
| Expression::Return { .. }
| Expression::Break { .. }
| Expression::Continue { .. }
| Expression::Defer { .. }
| Expression::Task { .. }
| Expression::While { .. }
| Expression::WhileLet { .. }
| Expression::For { .. } => {}
unwrapped => match mode {
DiscardMode::Tail(tail_ctx) => {
check_discarded_tail(expression, *tail_ctx, module_id, store, facts)
}
DiscardMode::NonTail => emit_unused_at_leaf(unwrapped, module_id, store, facts),
},
}
}
fn descend_loop_break_values(
expression: &Expression,
mode: &DiscardMode<'_>,
module_id: &str,
store: &Store,
facts: &mut LocalFacts,
) {
match expression {
Expression::Break {
value: Some(value), ..
} => {
descend_discarded(value, mode, module_id, store, facts);
}
Expression::Loop { .. }
| Expression::While { .. }
| Expression::WhileLet { .. }
| Expression::For { .. }
| Expression::Function { .. }
| Expression::Lambda { .. }
| Expression::Task { .. }
| Expression::Defer { .. } => {}
_ => {
for child in expression.children() {
descend_loop_break_values(child, mode, module_id, store, facts);
}
}
}
}
fn emit_unused_at_leaf(leaf: &Expression, module_id: &str, store: &Store, facts: &mut LocalFacts) {
let span = leaf.get_span();
let is_literal = is_literal_or_negated_literal(leaf);
let ty = leaf.get_type();
let mut allowed_lints = callee_allowed_lints(leaf, module_id, store);
if is_channel_send(leaf) {
allowed_lints.push("unused_value".to_string());
}
emit_unused_expression(span, &ty, is_literal, &allowed_lints, facts);
}
fn check_discarded_tail(
item: &Expression,
tail_ctx: Option<&TailContext<'_>>,
module_id: &str,
store: &Store,
facts: &mut LocalFacts,
) {
let unwrapped = item.unwrap_parens();
let reported_ty = get_call_return_type(unwrapped).unwrap_or_else(|| unwrapped.get_type());
let kind = if reported_ty.is_result() {
MismatchedTailKind::Result
} else if reported_ty.is_option() {
MismatchedTailKind::Option
} else if reported_ty.is_partial() {
MismatchedTailKind::Partial
} else if reported_ty.is_unit()
|| reported_ty.is_ignored()
|| reported_ty.is_never()
|| reported_ty.is_variable()
|| reported_ty.is_error()
{
return;
} else {
MismatchedTailKind::Value
};
let allowed_lints = callee_allowed_lints(unwrapped, module_id, store);
let alias = kind.allow_alias();
if allowed_lints.iter().any(|s| s == alias) {
return;
}
let (expected_span, expected_ty) = match tail_ctx {
Some(ctx) => (ctx.expected_span, ctx.expected_ty.to_string()),
None => (item.get_span(), reported_ty.to_string()),
};
facts.add_discarded_tail(
item.get_span(),
reported_ty.to_string(),
expected_span,
expected_ty,
);
}
fn emit_unused_expression(
span: Span,
ty: &Type,
is_literal: bool,
allowed_lints: &[String],
facts: &mut LocalFacts,
) {
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() && !ty.is_error() {
Some(UnusedExpressionKind::Value)
} else {
None
};
if let Some(kind) = kind
&& !allowed_lints.iter().any(|s| s == kind.lint_name())
{
facts.add_unused_expression(span, kind);
}
}
fn is_statement_only(expression: &Expression) -> bool {
matches!(
expression,
Expression::Let { .. }
| Expression::Assignment { .. }
| Expression::Defer { .. }
| Expression::Task { .. }
| Expression::While { .. }
| Expression::WhileLet { .. }
| Expression::For { .. }
| Expression::Struct { .. }
| Expression::Enum { .. }
| Expression::ValueEnum { .. }
| Expression::TypeAlias { .. }
| Expression::Interface { .. }
| Expression::Function { .. }
| Expression::ImplBlock { .. }
| Expression::Const { .. }
| Expression::VariableDeclaration { .. }
| Expression::ModuleImport { .. }
| Expression::RawGo { .. }
| Expression::NoOp
)
}
fn is_literal_or_negated_literal(expression: &Expression) -> bool {
match expression {
Expression::Literal { .. } => true,
Expression::Unary {
operator: UnaryOperator::Negative,
expression,
..
} => matches!(expression.as_ref(), Expression::Literal { .. }),
_ => false,
}
}
fn callee_allowed_lints(expression: &Expression, module_id: &str, store: &Store) -> Vec<String> {
let Expression::Call {
expression: callee, ..
} = expression
else {
return vec![];
};
if let Expression::Identifier {
value, qualified, ..
} = callee.as_ref()
{
if let Some(q) = qualified
&& let Some(definition) = store.get_definition(q.as_str())
{
return definition.allowed_lints().to_vec();
}
let qualified_guess = if value.contains('.') {
value.to_string()
} else {
Symbol::from_parts(module_id, value).to_string()
};
if let Some(definition) = store.get_definition(&qualified_guess) {
return definition.allowed_lints().to_vec();
}
}
if let Expression::DotAccess {
expression: receiver,
member,
..
} = callee.as_ref()
{
let receiver_ty = receiver.get_type().strip_refs();
if let Type::Nominal { id, .. } = &receiver_ty {
let method_key = id.with_segment(member);
if let Some(definition) = store.get_definition(&method_key) {
return definition.allowed_lints().to_vec();
}
}
if let Some(module) = receiver.get_type().as_import_namespace() {
let method_key = Symbol::from_parts(module, member);
if let Some(definition) = store.get_definition(&method_key) {
return definition.allowed_lints().to_vec();
}
}
}
vec![]
}
fn is_channel_send(expression: &Expression) -> bool {
let Expression::Call {
expression: callee,
args,
..
} = expression
else {
return false;
};
let Expression::DotAccess {
expression: receiver,
member,
..
} = callee.as_ref()
else {
return false;
};
if member != "send" || args.len() != 1 {
return false;
}
let resolved = receiver.get_type().strip_refs();
matches!(resolved.get_name(), Some("Channel" | "Sender"))
}
fn get_call_return_type(expression: &Expression) -> Option<Type> {
let Expression::Call {
expression: callee, ..
} = expression
else {
return None;
};
callee
.get_type()
.unwrap_forall()
.get_function_ret()
.cloned()
}