nu_lint/
external_command.rs

1use std::collections::HashMap;
2
3use nu_protocol::ast::Expr;
4
5// Re-export Fix type for use by fix builders
6pub use crate::lint::Fix;
7use crate::{context::LintContext, lint::RuleViolation};
8
9/// Extract external command arguments as strings
10#[must_use]
11pub fn extract_external_args(
12    args: &[nu_protocol::ast::ExternalArgument],
13    context: &LintContext,
14) -> Vec<String> {
15    args.iter()
16        .map(|arg| match arg {
17            nu_protocol::ast::ExternalArgument::Regular(expr) => {
18                context.source[expr.span.start..expr.span.end].to_string()
19            }
20            nu_protocol::ast::ExternalArgument::Spread(expr) => {
21                format!("...{}", &context.source[expr.span.start..expr.span.end])
22            }
23        })
24        .collect()
25}
26
27/// Metadata about a builtin alternative to an external command
28pub struct BuiltinAlternative {
29    pub command: &'static str,
30    pub note: Option<&'static str>,
31}
32
33impl BuiltinAlternative {
34    #[must_use]
35    pub fn simple(command: &'static str) -> Self {
36        Self {
37            command,
38            note: None,
39        }
40    }
41
42    #[must_use]
43    pub fn with_note(command: &'static str, note: &'static str) -> Self {
44        Self {
45            command,
46            note: Some(note),
47        }
48    }
49}
50
51/// Type alias for a function that builds a fix for a specific external command
52pub type FixBuilder = fn(
53    cmd_text: &str,
54    alternative: &BuiltinAlternative,
55    args: &[nu_protocol::ast::ExternalArgument],
56    expr_span: nu_protocol::Span,
57    context: &LintContext,
58) -> Fix;
59
60/// Check for special command usage patterns that need custom suggestions
61fn get_custom_suggestion(
62    cmd_text: &str,
63    args: &[nu_protocol::ast::ExternalArgument],
64    context: &LintContext,
65) -> Option<(String, String)> {
66    match cmd_text {
67        "tail" => {
68            let args_text = extract_external_args(args, context);
69            if args_text.iter().any(|arg| arg == "--pid") {
70                let message = "Consider using Nushell's structured approach for process \
71                               monitoring instead of external 'tail --pid'"
72                    .to_string();
73                let suggestion = "Replace 'tail --pid $pid -f /dev/null' with Nushell process \
74                                  monitoring:\nwhile (ps | where pid == $pid | length) > 0 { \
75                                  sleep 1s }\n\nThis approach uses Nushell's built-in ps command \
76                                  with structured data filtering and is more portable across \
77                                  platforms."
78                    .to_string();
79                return Some((message, suggestion));
80            }
81        }
82        "hostname" => {
83            let args_text = extract_external_args(args, context);
84            if args_text.iter().any(|arg| arg == "-I") {
85                let message = "Consider using Nushell's structured approach for getting IP \
86                               addresses instead of external 'hostname -I'"
87                    .to_string();
88                let suggestion = "Replace 'hostname -I' with Nushell network commands:\nsys net | \
89                                  get ip\n\nThis approach uses Nushell's built-in sys net command \
90                                  to get IP addresses in a structured format. You can filter \
91                                  specific interfaces or addresses as needed."
92                    .to_string();
93                return Some((message, suggestion));
94            }
95        }
96        _ => {}
97    }
98    None
99}
100
101/// Detect external commands with builtin alternatives
102#[must_use]
103pub fn detect_external_commands<S: ::std::hash::BuildHasher>(
104    context: &LintContext,
105    rule_id: &'static str,
106    alternatives: &HashMap<&'static str, BuiltinAlternative, S>,
107    fix_builder: Option<FixBuilder>,
108) -> Vec<RuleViolation> {
109    context.collect_rule_violations(|expr, ctx| {
110        if let Expr::ExternalCall(head, args) = &expr.expr {
111            let cmd_text = &ctx.source[head.span.start..head.span.end];
112
113            // Check for custom suggestions first
114            if let Some((custom_message, custom_suggestion)) =
115                get_custom_suggestion(cmd_text, args, ctx)
116            {
117                return vec![
118                    RuleViolation::new_dynamic(rule_id, custom_message, expr.span)
119                        .with_suggestion_dynamic(custom_suggestion),
120                ];
121            }
122
123            // Check if this external command has a builtin alternative
124            if let Some(alternative) = alternatives.get(cmd_text) {
125                let message = format!(
126                    "Consider using Nushell's built-in '{}' instead of external '^{}'",
127                    alternative.command, cmd_text
128                );
129
130                let suggestion = match alternative.note {
131                    Some(note) => format!(
132                        "Replace '^{}' with built-in command: {}\nBuilt-in commands are more \
133                         portable, faster, and provide better error handling.\n\nNote: {note}",
134                        cmd_text, alternative.command
135                    ),
136                    None => format!(
137                        "Replace '^{}' with built-in command: {}\nBuilt-in commands are more \
138                         portable, faster, and provide better error handling.",
139                        cmd_text, alternative.command
140                    ),
141                };
142
143                let fix =
144                    fix_builder.map(|builder| builder(cmd_text, alternative, args, expr.span, ctx));
145
146                let violation = RuleViolation::new_dynamic(rule_id, message, expr.span)
147                    .with_suggestion_dynamic(suggestion);
148
149                let violation = match fix {
150                    Some(f) => violation.with_fix(f),
151                    None => violation,
152                };
153
154                return vec![violation];
155            }
156        }
157        vec![]
158    })
159}