use nu_protocol::{
Span, Type, VarId,
ast::{Call, Expr, Pipeline, PipelineElement},
};
use super::call::CallExt;
use crate::{ast::expression::ExpressionExt, context::LintContext};
#[derive(Debug, Clone)]
pub struct CommandCluster<'a> {
pub indices: Vec<usize>,
pub calls: Vec<&'a Call>,
pub span: Span,
}
impl CommandCluster<'_> {
#[must_use]
pub const fn len(&self) -> usize {
self.calls.len()
}
#[must_use]
pub fn first_index(&self) -> Option<usize> {
self.indices.first().copied()
}
#[must_use]
pub fn last_index(&self) -> Option<usize> {
self.indices.last().copied()
}
}
#[derive(Debug, Clone)]
pub struct ClusterConfig {
pub min_size: usize,
pub max_gap: usize,
pub allowed_gap_commands: Vec<&'static str>,
}
impl Default for ClusterConfig {
fn default() -> Self {
Self {
min_size: 2,
max_gap: 0,
allowed_gap_commands: Vec::new(),
}
}
}
impl ClusterConfig {
#[must_use]
pub const fn min_consecutive(min_size: usize) -> Self {
Self {
min_size,
max_gap: 0,
allowed_gap_commands: Vec::new(),
}
}
#[must_use]
pub const fn with_max_gap(mut self, max_gap: usize) -> Self {
self.max_gap = max_gap;
self
}
#[must_use]
pub fn with_allowed_gaps(mut self, commands: Vec<&'static str>) -> Self {
self.allowed_gap_commands = commands;
self
}
}
#[derive(Debug)]
pub struct CommandPair<'a> {
pub first: &'a Call,
pub second: &'a Call,
pub first_index: usize,
pub second_index: usize,
pub span: Span,
}
pub trait PipelineExt {
fn infer_param_type(&self, param_var_id: VarId, context: &LintContext) -> Option<Type>;
fn find_command_clusters<'a>(
&'a self,
command_name: &str,
context: &LintContext,
config: &ClusterConfig,
) -> Vec<CommandCluster<'a>>;
fn find_command_pairs<'a, F1, F2>(
&'a self,
context: &LintContext,
first_predicate: F1,
second_predicate: F2,
) -> Vec<CommandPair<'a>>
where
F1: Fn(&Call, &LintContext) -> bool,
F2: Fn(&Call, &LintContext) -> bool;
}
impl PipelineExt for Pipeline {
fn infer_param_type(&self, param_var_id: VarId, context: &LintContext) -> Option<Type> {
log::trace!(
"infer_param_type from pipeline: param_var_id={:?}, pipeline_elements={}",
param_var_id,
self.elements.len()
);
let result = self
.elements
.windows(2)
.find_map(|window| infer_from_pipeline_window(param_var_id, window, context));
log::trace!("infer_param_type from pipeline result: {result:?}");
result
}
fn find_command_clusters<'a>(
&'a self,
command_name: &str,
context: &LintContext,
config: &ClusterConfig,
) -> Vec<CommandCluster<'a>> {
let matches: Vec<(usize, &Call)> = self
.elements
.iter()
.enumerate()
.filter_map(|(idx, elem)| {
if let Expr::Call(call) = &elem.expr.expr
&& call.is_call_to_command(command_name, context)
{
return Some((idx, call.as_ref()));
}
None
})
.collect();
if matches.len() < config.min_size {
return Vec::new();
}
build_clusters_from_matches(&matches, self, context, config)
}
fn find_command_pairs<'a, F1, F2>(
&'a self,
context: &LintContext,
first_predicate: F1,
second_predicate: F2,
) -> Vec<CommandPair<'a>>
where
F1: Fn(&Call, &LintContext) -> bool,
F2: Fn(&Call, &LintContext) -> bool,
{
self.elements
.windows(2)
.enumerate()
.filter_map(|(idx, window)| {
let (Expr::Call(first), Expr::Call(second)) =
(&window[0].expr.expr, &window[1].expr.expr)
else {
return None;
};
(first_predicate(first, context) && second_predicate(second, context)).then(|| {
CommandPair {
first,
second,
first_index: idx,
second_index: idx + 1,
span: Span::new(window[0].expr.span.start, window[1].expr.span.end),
}
})
})
.collect()
}
}
fn infer_from_pipeline_window(
param_var_id: VarId,
window: &[PipelineElement],
context: &LintContext,
) -> Option<Type> {
let contains_param = window[0].expr.contains_variable(param_var_id);
log::trace!(
" Checking pipeline window: contains_param={}, first_expr={:?}, second_expr={:?}",
contains_param,
&window[0].expr.expr,
&window[1].expr.expr
);
let Expr::Call(call) = &window[1].expr.expr else {
log::trace!(" -> Not a call expression");
return None;
};
if !contains_param {
log::trace!(" -> Parameter not used in first element");
return None;
}
let decl = context.working_set.get_decl(call.decl_id);
let sig = decl.signature();
let Some((input_type, _)) = sig.input_output_types.first() else {
log::trace!(" -> No input/output types for '{}'", decl.name());
return None;
};
if matches!(input_type, Type::Any) {
log::trace!(
" -> Found call to '{}', but input_type is Any",
decl.name()
);
return None;
}
log::trace!(
" -> Found call to '{}', input_type={:?}",
decl.name(),
input_type
);
Some(input_type.clone())
}
fn build_clusters_from_matches<'a>(
matches: &[(usize, &'a Call)],
pipeline: &Pipeline,
context: &LintContext,
config: &ClusterConfig,
) -> Vec<CommandCluster<'a>> {
let mut clusters = Vec::new();
let mut current_indices = vec![matches[0].0];
let mut current_calls = vec![matches[0].1];
for window in matches.windows(2) {
let (prev_idx, _) = window[0];
let (curr_idx, curr_call) = window[1];
let gap = curr_idx - prev_idx - 1;
let valid_gap = gap == 0
|| (gap <= config.max_gap
&& is_valid_gap(pipeline, prev_idx + 1, curr_idx, context, config));
if valid_gap {
current_indices.push(curr_idx);
current_calls.push(curr_call);
} else {
if current_indices.len() >= config.min_size {
clusters.push(build_cluster(¤t_indices, ¤t_calls, pipeline));
}
current_indices = vec![curr_idx];
current_calls = vec![curr_call];
}
}
if current_indices.len() >= config.min_size {
clusters.push(build_cluster(¤t_indices, ¤t_calls, pipeline));
}
clusters
}
fn is_valid_gap(
pipeline: &Pipeline,
start: usize,
end: usize,
context: &LintContext,
config: &ClusterConfig,
) -> bool {
if config.allowed_gap_commands.is_empty() {
return false;
}
(start..end).all(|i| {
if let Expr::Call(call) = &pipeline.elements[i].expr.expr {
let name = call.get_call_name(context);
config.allowed_gap_commands.iter().any(|&cmd| cmd == name)
} else {
false
}
})
}
fn build_cluster<'a>(
indices: &[usize],
calls: &[&'a Call],
pipeline: &Pipeline,
) -> CommandCluster<'a> {
let first_idx = indices[0];
let last_idx = indices[indices.len() - 1];
let span = Span::new(
pipeline.elements[first_idx].expr.span.start,
pipeline.elements[last_idx].expr.span.end,
);
CommandCluster {
indices: indices.to_vec(),
calls: calls.to_vec(),
span,
}
}