use nu_protocol::{
Span,
ast::{Argument, Call, Expr, Expression},
};
use super::{block::BlockExt, declaration::CustomCommandDef, expression::ExpressionExt};
use crate::context::LintContext;
fn is_type_compatible(expected: &nu_protocol::Type, actual: &nu_protocol::Type) -> bool {
use nu_protocol::Type;
match (expected, actual) {
(e, a) if e == a => true,
(Type::Any, _) | (_, Type::Any) => true,
(Type::List(expected_inner), Type::List(actual_inner)) => {
is_type_compatible(expected_inner, actual_inner)
}
_ => false,
}
}
pub trait CallExt {
fn get_call_name(&self, context: &LintContext) -> String;
fn is_call_to_command(&self, command_name: &str, context: &LintContext) -> bool;
fn get_first_positional_arg(&self) -> Option<&Expression>;
fn get_positional_arg(&self, index: usize) -> Option<&Expression>;
#[must_use]
fn loop_var_from_for(&self, context: &LintContext) -> Option<String>;
#[must_use]
fn custom_command_def(&self, context: &LintContext) -> Option<CustomCommandDef>;
#[must_use]
fn extract_variable_declaration(
&self,
context: &LintContext,
) -> Option<(nu_protocol::VarId, String, Span)>;
fn get_else_branch(&self) -> Option<(bool, &Expression)>;
fn uses_variable(&self, var_id: nu_protocol::VarId) -> bool;
fn is_filesystem_command(&self, context: &LintContext) -> bool;
fn has_named_flag(&self, flag_name: &str) -> bool;
fn is_get_optional(&self, context: &LintContext) -> bool;
fn get_for_loop_iterator(&self) -> Option<&Expression>;
fn get_for_loop_body(&self) -> Option<nu_protocol::BlockId>;
fn get_named_arg_expr(&self, flag_name: &str) -> Option<&Expression>;
fn is_branching_control_flow(&self, context: &LintContext) -> bool;
fn is_control_flow_command(&self, context: &LintContext) -> bool;
fn all_arg_expressions(&self) -> Vec<&Expression>;
fn get_output_type(
&self,
context: &LintContext,
pipeline_input: Option<nu_protocol::Type>,
) -> nu_protocol::Type;
fn infer_from_blocks(&self, context: &LintContext) -> Option<nu_protocol::Type>;
}
impl CallExt for Call {
fn get_output_type(
&self,
context: &LintContext,
pipeline_input: Option<nu_protocol::Type>,
) -> nu_protocol::Type {
let decl = context.working_set.get_decl(self.decl_id);
let sig = decl.signature();
log::trace!(
"get_output_type called for '{}': pipeline_input={pipeline_input:?}",
self.get_call_name(context)
);
log::trace!(
"Nu parser parsed output type for call '{}': {:?}",
self.get_call_name(context),
sig.get_output_type()
);
let has_pipeline_input = pipeline_input.is_some();
let input_type = pipeline_input.unwrap_or_else(|| sig.get_input_type());
log::trace!(
"Final input_type used for call '{}': {:?} (from pipeline_input: {})",
self.get_call_name(context),
input_type,
has_pipeline_input
);
log::trace!(
"Command '{}' input_output_types: {:?}",
self.get_call_name(context),
sig.input_output_types
);
for (in_ty, out_ty) in &sig.input_output_types {
if is_type_compatible(in_ty, &input_type) && !matches!(out_ty, nu_protocol::Type::Any) {
log::trace!(
"Found compatible type mapping for '{}': {:?} -> {:?} (actual input: {:?})",
self.get_call_name(context),
in_ty,
out_ty,
input_type
);
return out_ty.clone();
}
log::trace!(
"The signature with input type {:?} is not compatible with actual input type {:?} \
for command '{}'",
in_ty,
input_type,
self.get_call_name(context)
);
}
log::trace!(
"Could not find compatible type mapping for '{}'",
self.get_call_name(context)
);
if self.is_branching_control_flow(context)
&& let Some(inferred) = self.infer_from_blocks(context)
{
log::trace!(
"Branching control flow '{}' inferred output type from blocks: {:?}",
self.get_call_name(context),
inferred
);
return inferred;
}
sig.get_output_type()
}
fn get_call_name(&self, context: &LintContext) -> String {
context
.working_set
.get_decl(self.decl_id)
.name()
.to_string()
}
fn is_call_to_command(&self, command_name: &str, context: &LintContext) -> bool {
self.get_call_name(context) == command_name
}
fn get_first_positional_arg(&self) -> Option<&Expression> {
self.get_positional_arg(0)
}
fn get_positional_arg(&self, index: usize) -> Option<&Expression> {
self.arguments
.iter()
.filter_map(|arg| match arg {
Argument::Positional(expr) | Argument::Unknown(expr) => Some(expr),
_ => None,
})
.nth(index)
}
fn loop_var_from_for(&self, context: &LintContext) -> Option<String> {
let var_arg = self.get_first_positional_arg()?;
var_arg.extract_variable_name(context)
}
fn custom_command_def(&self, context: &LintContext) -> Option<CustomCommandDef> {
CustomCommandDef::try_from_call(self, context)
}
fn extract_variable_declaration(
&self,
context: &LintContext,
) -> Option<(nu_protocol::VarId, String, Span)> {
let decl_name = self.get_call_name(context);
if !matches!(decl_name.as_str(), "let" | "mut") {
return None;
}
let var_arg = self.get_first_positional_arg()?;
if let Expr::VarDecl(var_id) = &var_arg.expr {
let var_name = context.expr_text(var_arg);
Some((*var_id, var_name.to_string(), var_arg.span))
} else {
None
}
}
fn get_else_branch(&self) -> Option<(bool, &Expression)> {
let else_arg = self.get_positional_arg(2)?;
match &else_arg.expr {
Expr::Keyword(keyword) => match &keyword.expr.expr {
Expr::Call(_) => Some((true, &keyword.expr)),
Expr::Block(_) => Some((false, &keyword.expr)),
_ => None,
},
Expr::Block(_) => Some((false, else_arg)),
_ => None,
}
}
fn uses_variable(&self, var_id: nu_protocol::VarId) -> bool {
self.arguments.iter().any(|arg| match arg {
Argument::Positional(expr)
| Argument::Unknown(expr)
| Argument::Named((_, _, Some(expr))) => expr.matches_var(var_id),
_ => false,
})
}
fn is_filesystem_command(&self, context: &LintContext) -> bool {
use nu_protocol::Category;
let decl = context.working_set.get_decl(self.decl_id);
let signature = decl.signature();
matches!(signature.category, Category::FileSystem | Category::Path)
}
fn has_named_flag(&self, flag_name: &str) -> bool {
self.arguments.iter().any(|arg| {
matches!(
arg,
Argument::Named(named) if named.0.item == flag_name
)
})
}
fn is_get_optional(&self, context: &LintContext) -> bool {
self.is_call_to_command("get", context)
&& (self.has_named_flag("optional")
|| self.has_named_flag("o")
|| self.has_named_flag("ignore-errors")
|| self.has_named_flag("i"))
}
fn get_for_loop_iterator(&self) -> Option<&Expression> {
self.get_positional_arg(1)
}
fn get_for_loop_body(&self) -> Option<nu_protocol::BlockId> {
self.arguments.last().and_then(|arg| match arg {
Argument::Positional(expr) | Argument::Unknown(expr) => expr.extract_block_id(),
_ => None,
})
}
fn get_named_arg_expr(&self, flag_name: &str) -> Option<&Expression> {
self.arguments.iter().find_map(|arg| {
if let Argument::Named(named) = arg
&& named.0.item == flag_name
{
named.2.as_ref()
} else {
None
}
})
}
fn is_branching_control_flow(&self, context: &LintContext) -> bool {
matches!(self.get_call_name(context).as_str(), "if" | "match" | "try")
}
fn is_control_flow_command(&self, context: &LintContext) -> bool {
matches!(
self.get_call_name(context).as_str(),
"if" | "for" | "while" | "loop" | "match" | "try"
)
}
fn all_arg_expressions(&self) -> Vec<&Expression> {
self.arguments
.iter()
.filter_map(|arg| match arg {
Argument::Positional(e) | Argument::Unknown(e) | Argument::Spread(e) => Some(e),
Argument::Named(named) => named.2.as_ref(),
})
.collect()
}
fn infer_from_blocks(&self, context: &LintContext) -> Option<nu_protocol::Type> {
log::trace!("Inferring type from call with blocks");
let mut block_types = self.positional_iter().filter_map(|arg| {
arg.extract_block_id().map(|block_id| {
let output = context
.working_set
.get_block(block_id)
.infer_output_type(context);
log::trace!("Block {block_id:?} output type: {output:?}");
output
})
});
let first = block_types.next()?;
log::trace!("First block type: {first:?}");
let unified = block_types.try_fold(first, |acc, ty| {
if acc == ty {
Some(acc)
} else {
log::trace!("Block types differ: {acc:?} vs {ty:?}");
None
}
})?;
log::trace!("Unified block type: {unified:?}");
Some(unified)
}
}