nu_lint/
external_command.rs1use std::{collections::HashMap, fmt::Write};
5
6use nu_protocol::ast::Expr;
7
8pub use crate::lint::Fix;
10use crate::{
11 lint::{Severity, Violation},
12 visitor::{AstVisitor, VisitContext},
13};
14
15pub 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
39pub 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
48pub 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 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 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 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 if let Expr::ExternalCall(head, args) = &expr.expr {
128 let cmd_text = context.get_span_contents(head.span);
130
131 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, file: None,
143 });
144 return;
145 }
146
147 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 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}