nu_lint/
external_command.rs

1use std::{collections::HashMap, fmt::Write};
2
3use nu_protocol::ast::Expr;
4
5// Re-export Fix type for use by fix builders
6pub use crate::lint::Fix;
7use crate::{
8    lint::{Severity, Violation},
9    visitor::{AstVisitor, VisitContext},
10};
11
12/// Metadata about a builtin alternative to an external command
13pub struct BuiltinAlternative {
14    pub command: &'static str,
15    pub note: Option<&'static str>,
16}
17
18impl BuiltinAlternative {
19    #[must_use]
20    pub fn simple(command: &'static str) -> Self {
21        Self {
22            command,
23            note: None,
24        }
25    }
26
27    #[must_use]
28    pub fn with_note(command: &'static str, note: &'static str) -> Self {
29        Self {
30            command,
31            note: Some(note),
32        }
33    }
34}
35
36/// Type alias for a function that builds a fix for a specific external command
37pub type FixBuilder = fn(
38    cmd_text: &str,
39    alternative: &BuiltinAlternative,
40    args: &[nu_protocol::ast::ExternalArgument],
41    expr_span: nu_protocol::Span,
42    context: &VisitContext,
43) -> Fix;
44
45/// Generic AST visitor for detecting external commands with builtin
46/// alternatives
47pub struct ExternalCommandVisitor {
48    rule_id: String,
49    severity: Severity,
50    violations: Vec<Violation>,
51    alternatives: HashMap<&'static str, BuiltinAlternative>,
52    fix_builder: Option<FixBuilder>,
53}
54
55impl ExternalCommandVisitor {
56    #[must_use]
57    pub fn new(
58        rule_id: &str,
59        severity: Severity,
60        alternatives: HashMap<&'static str, BuiltinAlternative>,
61        fix_builder: Option<FixBuilder>,
62    ) -> Self {
63        Self {
64            rule_id: rule_id.to_string(),
65            severity,
66            violations: Vec::new(),
67            alternatives,
68            fix_builder,
69        }
70    }
71
72    #[must_use]
73    pub fn into_violations(self) -> Vec<Violation> {
74        self.violations
75    }
76
77    /// Check for special command usage patterns that need custom suggestions
78    fn get_custom_suggestion(
79        cmd_text: &str,
80        args: &[nu_protocol::ast::ExternalArgument],
81        context: &VisitContext,
82    ) -> Option<(String, String)> {
83        match cmd_text {
84            "tail" => {
85                let args_text = context.extract_external_args(args);
86                if args_text.iter().any(|arg| arg == "--pid") {
87                    // Special case for tail --pid - this is process monitoring
88                    let message = "Consider using Nushell's structured approach for process \
89                                   monitoring instead of external 'tail --pid'"
90                        .to_string();
91                    let suggestion = "Replace 'tail --pid $pid -f /dev/null' with Nushell process \
92                                      monitoring:\nwhile (ps | where pid == $pid | length) > 0 { \
93                                      sleep 1s }\n\nThis approach uses Nushell's built-in ps \
94                                      command with structured data filtering and is more portable \
95                                      across platforms."
96                        .to_string();
97                    return Some((message, suggestion));
98                }
99            }
100            "hostname" => {
101                let args_text = context.extract_external_args(args);
102                if args_text.iter().any(|arg| arg == "-I") {
103                    // Special case for hostname -I - this gets IP addresses, not hostname
104                    let message = "Consider using Nushell's structured approach for getting IP \
105                                   addresses instead of external 'hostname -I'"
106                        .to_string();
107                    let suggestion = "Replace 'hostname -I' with Nushell network commands:\nsys \
108                                      net | get ip\n\nThis approach uses Nushell's built-in sys \
109                                      net command to get IP addresses in a structured format. You \
110                                      can filter specific interfaces or addresses as needed."
111                        .to_string();
112                    return Some((message, suggestion));
113                }
114            }
115            _ => {}
116        }
117        None
118    }
119}
120
121impl AstVisitor for ExternalCommandVisitor {
122    fn visit_expression(&mut self, expr: &nu_protocol::ast::Expression, context: &VisitContext) {
123        // Check for external calls
124        if let Expr::ExternalCall(head, args) = &expr.expr {
125            // Get the command name from the head expression
126            let cmd_text = context.get_span_contents(head.span);
127
128            // Check for custom suggestions first
129            if let Some((custom_message, custom_suggestion)) =
130                Self::get_custom_suggestion(cmd_text, args, context)
131            {
132                self.violations.push(Violation {
133                    rule_id: self.rule_id.clone().into(),
134                    severity: self.severity,
135                    message: custom_message.into(),
136                    span: expr.span,
137                    suggestion: Some(custom_suggestion.into()),
138                    fix: None, // Custom suggestions don't have automatic fixes yet
139                    file: None,
140                });
141                return;
142            }
143
144            // Check if this external command has a builtin alternative
145            if let Some(alternative) = self.alternatives.get(cmd_text) {
146                let message = format!(
147                    "Consider using Nushell's built-in '{}' instead of external '^{}'",
148                    alternative.command, cmd_text
149                );
150
151                let mut suggestion = format!(
152                    "Replace '^{}' with built-in command: {}\nBuilt-in commands are more \
153                     portable, faster, and provide better error handling.",
154                    cmd_text, alternative.command
155                );
156
157                if let Some(note) = alternative.note {
158                    write!(suggestion, "\n\nNote: {note}").unwrap();
159                }
160
161                // Build fix if a fix builder is provided
162                let fix = self
163                    .fix_builder
164                    .map(|builder| builder(cmd_text, alternative, args, expr.span, context));
165
166                self.violations.push(Violation {
167                    rule_id: self.rule_id.clone().into(),
168                    severity: self.severity,
169                    message: message.into(),
170                    span: expr.span,
171                    suggestion: Some(suggestion.into()),
172                    fix,
173                    file: None,
174                });
175            }
176        }
177
178        crate::visitor::walk_expression(self, expr, context);
179    }
180}