nu_lint/
external_command.rs1use std::{collections::HashMap, fmt::Write};
2
3use nu_protocol::ast::Expr;
4
5pub use crate::lint::Fix;
7use crate::{
8 lint::{Severity, Violation},
9 visitor::{AstVisitor, VisitContext},
10};
11
12pub 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
36pub 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
45pub 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 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 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 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 if let Expr::ExternalCall(head, args) = &expr.expr {
125 let cmd_text = context.get_span_contents(head.span);
127
128 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, file: None,
140 });
141 return;
142 }
143
144 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 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}