nu_lint/
external_command.rs

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