knave 0.12.2

A Lisp programming language for humans and agents that infers types and ownership and compiles to readable Rust
Documentation
use rustc_hash::FxHashSet;

use crate::core::BindingId;
use crate::diagnostic::{Diagnostic, Diagnostics};
use crate::types::{
    CopyRules, ReceiverKind, Type, TypedDefinition, TypedExprKind, TypedLambda, TypedProgram,
    TypedTraitMethod,
};

use super::problems::OwnershipProblems;
use super::whole_value_changes::WholeValueChanges;

pub struct ChangingFunctionNames;

impl ChangingFunctionNames {
    pub fn check(program: &TypedProgram) -> Diagnostics {
        let impl_methods = Self::impl_methods(program);
        let default_methods = Self::default_methods(program);
        let mut warnings = Diagnostics::default();
        for definition in program
            .modules
            .iter()
            .flat_map(|module| &module.definitions)
        {
            if impl_methods.contains(&definition.binding)
                || default_methods.contains(&definition.binding)
            {
                continue;
            }
            if let Some(warning) = Self::function_warning(program, definition) {
                warnings.push(warning);
            }
        }
        for method in program
            .trait_definitions
            .iter()
            .flat_map(|definition| &definition.methods)
        {
            if let Some(warning) = Self::trait_method_warning(program, method) {
                warnings.push(warning);
            }
        }
        warnings
    }

    pub(super) fn impl_methods(program: &TypedProgram) -> FxHashSet<BindingId> {
        program
            .trait_impls
            .iter()
            .flat_map(|implementation| implementation.methods.iter())
            .map(|method| method.binding)
            .collect()
    }

    pub(super) fn default_methods(program: &TypedProgram) -> FxHashSet<BindingId> {
        program
            .trait_definitions
            .iter()
            .flat_map(|definition| definition.methods.iter())
            .filter_map(|method| method.default)
            .collect()
    }

    fn function_warning(
        program: &TypedProgram,
        definition: &TypedDefinition,
    ) -> Option<Diagnostic> {
        let TypedExprKind::Fn(lambda) = &definition.value.kind else {
            return None;
        };
        let changes_argument = lambda
            .params
            .iter()
            .any(|param| matches!(&param.ty, Type::Mut(_, _)));
        Self::changed_name_warning(
            program,
            definition.binding,
            definition.span,
            changes_argument,
        )
        .map(|warning| Self::with_replaced_parameter(warning, program, lambda))
    }

    fn with_replaced_parameter(
        warning: Diagnostic,
        program: &TypedProgram,
        lambda: &TypedLambda,
    ) -> Diagnostic {
        let replaced = lambda.params.iter().find_map(|param| match &param.ty {
            Type::Mut(_, referent) => WholeValueChanges::site(lambda, param.binding)
                .map(|site| (param.binding, referent, site)),
            _ => None,
        });
        let Some((param, referent, site)) = replaced else {
            return warning;
        };
        let name = program.bindings.get(param).name();
        let copy = CopyRules::new(
            &program.traits,
            &program.generics,
            &program.structs,
            &program.enums,
        );
        if copy.is_copy(referent) {
            OwnershipProblems::with_copied_parameter_change(warning, site, name)
        } else {
            OwnershipProblems::with_cloned_parameter_change(warning, site, name)
        }
    }

    fn trait_method_warning(
        program: &TypedProgram,
        method: &TypedTraitMethod,
    ) -> Option<Diagnostic> {
        let definition = Self::definition(program, method.binding)?;
        let changes_argument =
            method.receiver == Some(ReceiverKind::Mutable) || Self::takes_mut_parameter(definition);
        Self::changed_name_warning(program, method.binding, definition.span, changes_argument)
    }

    fn definition(program: &TypedProgram, binding: BindingId) -> Option<&TypedDefinition> {
        program
            .modules
            .iter()
            .flat_map(|module| &module.definitions)
            .find(|definition| definition.binding == binding)
    }

    fn takes_mut_parameter(definition: &TypedDefinition) -> bool {
        match &definition.scheme.body {
            Type::Function(_, parameters, _) => parameters
                .iter()
                .any(|parameter| matches!(parameter, Type::Mut(..))),
            _ => false,
        }
    }

    fn changed_name_warning(
        program: &TypedProgram,
        binding: BindingId,
        definition_span: crate::source::Span,
        changes_argument: bool,
    ) -> Option<Diagnostic> {
        let info = program.bindings.get(binding);
        let span = info.span().unwrap_or(definition_span);
        (changes_argument && !info.name().ends_with('!'))
            .then(|| OwnershipProblems::missing_bang(info.name(), span))
    }
}