harn-lint 0.10.133

Linter for the Harn programming language
//! Declaration policy for removable pipeline inputs.

use harn_lexer::{FixEdit, Span};
use harn_parser::{
    lexical::{resolved_identifier_bindings_with_source, BindingId},
    SNode, TypedParam,
};

use super::Linter;
use crate::decls::{ParamDeclaration, RemovablePipelineParam};
use crate::fixes::pipeline_parameter_removal_fix;

impl Linter<'_> {
    pub(super) fn declare_pipeline_parameters(
        &mut self,
        params: &[TypedParam],
        body: &[SNode],
        owner: &str,
        removal_allowed: bool,
    ) {
        // An unattributed declaration owns its own arity. `harn test` derives
        // one Nil argument per declared non-`Harness` parameter, so a
        // `test_*` pipeline runs identically whether or not it keeps a
        // vestigial trailing input; the slot is removable like any other. An
        // attributed declaration is different: `@test(cases: ...)` rows and
        // `@test(fixture: ...)` count the slot, so only a bare `@test` allows
        // removal.
        let removal_allowed =
            removal_allowed && self.pipeline_input_attribute_owner.unwrap_or(true);
        let used: std::collections::HashSet<_> = if removal_allowed
            && params
                .iter()
                .any(|parameter| parameter.name.starts_with('_'))
        {
            resolved_identifier_bindings_with_source(
                params,
                body,
                self.source,
                &self.match_patterns,
            )
            .into_values()
            .collect()
        } else {
            Default::default()
        };
        for (index, parameter) in params.iter().enumerate() {
            let removable = removal_allowed
                && !parameter.rest
                && parameter.default_value.is_none()
                && !used.contains(&BindingId::from_declaration(
                    &parameter.name,
                    parameter.span,
                ))
                && parameter.name.starts_with('_');
            let removal = removable
                .then(|| pipeline_parameter_removal_fix(self.source, params, index))
                .flatten();
            if let Some((fix, fix_after_removed_previous)) = removal {
                self.declare_removable_pipeline_parameter(
                    &parameter.name,
                    parameter.span,
                    owner,
                    fix,
                    index
                        .checked_sub(1)
                        .map(|previous| params[previous].name.clone()),
                    fix_after_removed_previous,
                );
            } else {
                self.declare_parameter(&parameter.name, parameter.span);
                // A non-removable pipeline slot belongs to an invocation
                // contract outside this declaration. Its body cannot prove
                // the slot unused, so do not manufacture a rename warning.
                self.references.insert(parameter.name.clone());
            }
        }
    }

    /// Declare an explicitly unused pipeline input whose positional slot can
    /// be deleted when no caller reference survives the full walk.
    pub(super) fn declare_removable_pipeline_parameter(
        &mut self,
        name: &str,
        span: Span,
        owner: &str,
        fix: Vec<FixEdit>,
        previous_name: Option<String>,
        fix_after_removed_previous: Option<Vec<FixEdit>>,
    ) {
        if name == "_" {
            return;
        }
        self.warn_if_shadows_outer_scope(name, span);
        if let Some(scope) = self.scopes.last_mut() {
            scope.insert(name.to_string());
        }
        self.param_declarations.push(ParamDeclaration {
            name: name.to_string(),
            span,
            removable_pipeline: Some(RemovablePipelineParam {
                owner: owner.to_string(),
                fix,
                previous_name,
                fix_after_removed_previous,
            }),
        });
    }
}