Skip to main content

nu_parser/
parse_keywords.rs

1use crate::{lite_parser::LiteCommand, parser::parse_call};
2use nu_protocol::{
3    DeclId, ParseError, Span, Type,
4    ast::{Expr, Expression, Pipeline},
5    engine::StateWorkingSet,
6};
7
8/// These parser keywords can be aliased
9pub const ALIASABLE_PARSER_KEYWORDS: &[&[u8]] = &[
10    b"if",
11    b"match",
12    b"try",
13    b"overlay",
14    b"overlay hide",
15    b"overlay new",
16    b"overlay use",
17];
18
19/// These parser keywords cannot be aliased (either not possible, or support not yet added)
20pub const UNALIASABLE_PARSER_KEYWORDS: &[&[u8]] = &[
21    b"alias",
22    b"const",
23    b"def",
24    b"extern",
25    b"module",
26    b"use",
27    b"export",
28    b"export alias",
29    b"export const",
30    b"export def",
31    b"export extern",
32    b"export module",
33    b"export use",
34    b"for",
35    b"loop",
36    b"while",
37    b"return",
38    b"break",
39    b"continue",
40    b"let",
41    b"mut",
42    b"hide",
43    b"export-env",
44    b"source-env",
45    b"source",
46    b"run",
47    b"where",
48    b"plugin use",
49];
50
51/// Check whether spans start with a parser keyword that can be aliased
52pub fn is_unaliasable_parser_keyword(working_set: &StateWorkingSet, spans: &[Span]) -> bool {
53    // try two words
54    if let (Some(&span1), Some(&span2)) = (spans.first(), spans.get(1)) {
55        let cmd_name = working_set.get_span_contents(Span::append(span1, span2));
56        return UNALIASABLE_PARSER_KEYWORDS.contains(&cmd_name);
57    }
58
59    // try one word
60    if let Some(&span1) = spans.first() {
61        let cmd_name = working_set.get_span_contents(span1);
62        UNALIASABLE_PARSER_KEYWORDS.contains(&cmd_name)
63    } else {
64        false
65    }
66}
67
68/// Returns true if `name` matches any parser keyword (aliasable or unaliasable).
69///
70/// Used to prevent custom commands and aliases from shadowing language keywords
71/// (e.g. `def def [] {}`), which can break subsequent parsing and previously
72/// panicked in the REPL. This applies everywhere, including module exports —
73/// `use mod *` would otherwise bring a bare keyword-named command into scope.
74///
75/// Also used when a module is named after a keyword and exports `main`: invoking
76/// that entry point would use the module name as a bare command, which the parser
77/// intercepts before command lookup.
78///
79/// Multi-word keywords such as `export def` and `overlay use` are included in the
80/// lists; they will not match normal single-token definition or module names. See
81/// [`single_word_parser_keywords`].
82pub fn is_parser_keyword(name: &[u8]) -> bool {
83    ALIASABLE_PARSER_KEYWORDS.contains(&name) || UNALIASABLE_PARSER_KEYWORDS.contains(&name)
84}
85
86/// Single-token parser keyword names that cannot be used as command or alias names.
87///
88/// Multi-word entries (`export def`, `overlay use`, …) are omitted because they
89/// cannot appear as a single definition name token.
90pub fn single_word_parser_keywords() -> impl Iterator<Item = &'static str> {
91    ALIASABLE_PARSER_KEYWORDS
92        .iter()
93        .chain(UNALIASABLE_PARSER_KEYWORDS.iter())
94        .filter_map(|bytes| {
95            let name = std::str::from_utf8(bytes).ok()?;
96            (!name.contains(' ')).then_some(name)
97        })
98}
99
100/// If `name` is a parser keyword, records [`ParseError::NameIsKeyword`] and returns `true`.
101///
102/// `kind` is embedded in the error (e.g. `"command"` or `"alias"`). Callers should
103/// abort the definition when this returns `true`.
104pub fn reject_parser_keyword_name(
105    working_set: &mut StateWorkingSet,
106    name: &str,
107    kind: &str,
108    span: Span,
109) -> bool {
110    if is_parser_keyword(name.as_bytes()) {
111        working_set.error(ParseError::NameIsKeyword(
112            name.to_owned(),
113            kind.to_owned(),
114            span,
115        ));
116        true
117    } else {
118        false
119    }
120}
121
122/// Find a keyword declaration by name, ignoring any non-keyword decls that may
123/// have shadowed it in normal name lookup.
124///
125/// Prefer this when resolving parser-keyword commands such as `def`, `extern`,
126/// or `run`, so a user-defined command of the same name cannot hijack parsing.
127pub(crate) fn find_keyword_decl(working_set: &StateWorkingSet, name: &[u8]) -> Option<DeclId> {
128    (0..working_set.num_decls())
129        .map(DeclId::new)
130        .find(|decl_id| {
131            let decl = working_set.get_decl(*decl_id);
132            decl.name().as_bytes() == name && decl.is_keyword()
133        })
134}
135
136/// This is a new more compact method of calling parse_xxx() functions without repeating the
137/// parse_call() in each function. Remaining keywords can be moved here.
138pub fn parse_keyword(working_set: &mut StateWorkingSet, lite_command: &LiteCommand) -> Pipeline {
139    let orig_parse_errors_len = working_set.parse_errors.len();
140
141    let call_expr = parse_call(
142        working_set,
143        &lite_command.parts,
144        lite_command.parts[0],
145        None,
146    );
147
148    // If an error occurred, don't invoke the keyword-specific functionality
149    if working_set.parse_errors.len() > orig_parse_errors_len {
150        return Pipeline::from_vec(vec![call_expr]);
151    }
152
153    if let Expression {
154        expr: Expr::Call(call),
155        ..
156    } = call_expr.clone()
157    {
158        // Apply parse keyword side effects
159        let cmd = working_set.get_decl(call.decl_id);
160        // check help flag first.
161        if call.named_iter().any(|(flag, _, _)| flag.item == "help") {
162            let call_span = call.span();
163            return Pipeline::from_vec(vec![Expression::new(
164                working_set,
165                Expr::Call(call),
166                call_span,
167                Type::Any,
168            )]);
169        }
170
171        match cmd.name() {
172            "overlay hide" => crate::parse_module::parse_overlay_hide(working_set, call),
173            "overlay new" => crate::parse_module::parse_overlay_new(working_set, call),
174            "overlay use" => crate::parse_module::parse_overlay_use(working_set, call),
175            #[cfg(feature = "plugin")]
176            "plugin use" => crate::parse_source::parse_plugin_use(working_set, call),
177            _ => Pipeline::from_vec(vec![call_expr]),
178        }
179    } else {
180        Pipeline::from_vec(vec![call_expr])
181    }
182}
183
184// Re-exports
185pub use crate::parse_alias::parse_alias;
186pub use crate::parse_bindings::{parse_const, parse_let, parse_mut};
187pub use crate::parse_def::{
188    parse_attribute_block, parse_def, parse_def_predecl, parse_extern, parse_for,
189};
190pub use crate::parse_module::{
191    parse_export_env, parse_export_in_block, parse_export_in_module, parse_hide, parse_module,
192    parse_module_block, parse_module_file_or_dir, parse_overlay_hide, parse_overlay_new,
193    parse_overlay_use, parse_use,
194};
195pub use crate::parse_source::{
196    LIB_DIRS_VAR, find_dirs_var, find_in_dirs, find_main_block_id_in_script, parse_run,
197    parse_run_expr, parse_source, parse_where, parse_where_expr,
198};
199#[cfg(feature = "plugin")]
200pub use crate::parse_source::{PLUGIN_DIRS_VAR, parse_plugin_use};
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205
206    #[test]
207    fn is_parser_keyword_matches_single_word_keywords() {
208        assert!(is_parser_keyword(b"def"));
209        assert!(is_parser_keyword(b"let"));
210        assert!(is_parser_keyword(b"if")); // aliasable
211        assert!(is_parser_keyword(b"overlay")); // aliasable
212        assert!(is_parser_keyword(b"where"));
213    }
214
215    #[test]
216    fn is_parser_keyword_rejects_ordinary_command_names() {
217        assert!(!is_parser_keyword(b"ls"));
218        assert!(!is_parser_keyword(b"my-command"));
219        assert!(!is_parser_keyword(b""));
220    }
221
222    #[test]
223    fn is_parser_keyword_includes_multi_word_entries() {
224        // Present in the tables; normal `def`/`alias` names are single tokens so
225        // these only matter for completeness of the keyword set.
226        assert!(is_parser_keyword(b"export def"));
227        assert!(is_parser_keyword(b"overlay use"));
228    }
229
230    #[test]
231    fn single_word_parser_keywords_excludes_multi_word_and_matches_is_parser_keyword() {
232        let names: Vec<_> = single_word_parser_keywords().collect();
233        assert!(names.contains(&"def"));
234        assert!(names.contains(&"if"));
235        assert!(!names.iter().any(|n| n.contains(' ')));
236        for name in &names {
237            assert!(
238                is_parser_keyword(name.as_bytes()),
239                "{name} should be a parser keyword"
240            );
241        }
242    }
243}