Skip to main content

nu_protocol/errors/
parse_error.rs

1#![allow(unused_assignments)]
2use crate::{Span, Type, ast::RedirectionSource, did_you_mean};
3use miette::Diagnostic;
4use serde::{Deserialize, Serialize};
5use std::{
6    fmt::Display,
7    str::{Utf8Error, from_utf8},
8};
9use thiserror::Error;
10
11#[derive(Clone, Debug, Error, Diagnostic, Serialize, Deserialize, PartialEq)]
12pub enum ParseError {
13    /// The parser encountered unexpected tokens, when the code should have
14    /// finished. You should remove these or finish adding what you intended
15    /// to add.
16    #[error("Extra tokens in code.")]
17    #[diagnostic(
18        code(nu::parser::extra_tokens),
19        help(
20            "Remove the unexpected tokens, or check for a missing operator, delimiter, or newline above."
21        )
22    )]
23    ExtraTokens(#[label = "extra tokens"] Span),
24
25    #[error("Invalid characters after closing delimiter")]
26    #[diagnostic(
27        code(nu::parser::extra_token_after_closing_delimiter),
28        help("Remove these characters, or check that a delimiter closed too early above.")
29    )]
30    ExtraTokensAfterClosingDelimiter(#[label = "invalid characters"] Span),
31
32    #[error("Extra positional argument.")]
33    #[diagnostic(code(nu::parser::extra_positional), help("Usage: {0}"))]
34    ExtraPositional(String, #[label = "extra positional argument"] Span),
35
36    #[error("Required positional parameter after optional parameter")]
37    #[diagnostic(code(nu::parser::required_after_optional))]
38    RequiredAfterOptional(
39        String,
40        #[label = "required parameter {0} after optional parameter"] Span,
41    ),
42
43    #[error("Unexpected end of code.")]
44    #[diagnostic(code(nu::parser::unexpected_eof))]
45    UnexpectedEof(String, #[label("expected closing {0}")] Span),
46
47    /// A delimiter was opened but never closed.
48    ///
49    /// - `0`: expected closer (e.g. `"}"`, `"]"`, `")"`)
50    /// - `1`: span of the **opening** delimiter
51    /// - `2`: span where the closer was expected (primary — often mid-file or EOF)
52    /// - `3`: help text (generic or heuristic structure hint)
53    ///
54    /// Primary label is the **expected closer** location so a deleted `}` near
55    /// line N points the user at (or near) line N, not only at a distant opener.
56    #[error("Unclosed delimiter.")]
57    #[diagnostic(code(nu::parser::unclosed_delimiter), help("{3}"))]
58    Unclosed(
59        &'static str,
60        #[label("unclosed — opens here (need `{0}`)")] Span,
61        #[label(primary, "expected `{0}` here")] Span,
62        String,
63    ),
64
65    /// A closer appeared without a matching opener (or mismatched kind).
66    ///
67    /// - `0` / `1`: open and close delimiter characters (e.g. `"{"`, `"}"`)
68    /// - `2`: span of the unexpected closer (primary label)
69    /// - `3`: help text
70    #[error("Unbalanced delimiter.")]
71    #[diagnostic(code(nu::parser::unbalanced_delimiter), help("{3}"))]
72    Unbalanced(
73        &'static str,
74        &'static str,
75        #[label("unexpected `{1}` (unbalanced with `{0}`)")] Span,
76        String,
77    ),
78
79    #[error("Parse mismatch: expected {0}.")]
80    #[diagnostic(
81        code(nu::parser::parse_mismatch),
82        help(
83            "Check the syntax around this position — a typo, missing delimiter, or wrong separator is common."
84        )
85    )]
86    Expected(&'static str, #[label("expected {0}")] Span),
87
88    #[error("Parse mismatch: expected {0}.")]
89    #[diagnostic(
90        code(nu::parser::parse_mismatch_with_full_string_msg),
91        help(
92            "Check the syntax around this position — a typo, missing delimiter, or wrong separator is common."
93        )
94    )]
95    ExpectedWithStringMsg(String, #[label("expected {0}")] Span),
96
97    #[error("Parse mismatch: expected {0}.")]
98    #[diagnostic(
99        code(nu::parser::parse_mismatch_with_did_you_mean),
100        help(
101            "Check the syntax around this position — a typo, missing delimiter, or wrong separator is common."
102        )
103    )]
104    ExpectedWithDidYouMean(&'static str, DidYouMean, #[label("expected {0}. {1}")] Span),
105
106    #[error("Command does not support {0} input.")]
107    #[diagnostic(code(nu::parser::input_type_mismatch))]
108    InputMismatch(String, #[label("command doesn't support {0} input")] Span),
109
110    #[error("Command output doesn't match {0}.")]
111    #[diagnostic(code(nu::parser::output_type_mismatch))]
112    OutputMismatch(
113        Type,
114        String,
115        #[label("expected {0}, but command outputs {1}")] Span,
116    ),
117
118    #[error("Type mismatch during operation.")]
119    #[diagnostic(code(nu::parser::type_mismatch))]
120    Mismatch(String, String, #[label("expected {0}, found {1}")] Span), // expected, found, span
121
122    #[error("The '&&' operator is not supported in Nushell")]
123    #[diagnostic(
124        code(nu::parser::shell_andand),
125        help("use ';' instead of the shell '&&', or 'and' instead of the boolean '&&'")
126    )]
127    ShellAndAnd(#[label("instead of '&&', use ';' or 'and'")] Span),
128
129    #[error("The '||' operator is not supported in Nushell")]
130    #[diagnostic(
131        code(nu::parser::shell_oror),
132        help("use 'try' instead of the shell '||', or 'or' instead of the boolean '||'")
133    )]
134    ShellOrOr(#[label("instead of '||', use 'try' or 'or'")] Span),
135
136    #[error("The '2>' shell operation is 'err>' in Nushell.")]
137    #[diagnostic(code(nu::parser::shell_err))]
138    ShellErrRedirect(#[label("use 'err>' instead of '2>' in Nushell")] Span),
139
140    #[error("The '2>&1' shell operation is 'out+err>' in Nushell.")]
141    #[diagnostic(
142        code(nu::parser::shell_outerr),
143        help("Nushell redirection will write all of stdout before stderr.")
144    )]
145    ShellOutErrRedirect(#[label("use 'out+err>' instead of '2>&1' in Nushell")] Span),
146
147    #[error("Multiple redirections provided for {0}.")]
148    #[diagnostic(code(nu::parser::multiple_redirections))]
149    MultipleRedirections(
150        RedirectionSource,
151        #[label = "first redirection"] Span,
152        #[label = "second redirection"] Span,
153    ),
154
155    #[error("Unexpected redirection.")]
156    #[diagnostic(code(nu::parser::unexpected_redirection))]
157    UnexpectedRedirection {
158        #[label = "redirecting nothing"]
159        span: Span,
160    },
161
162    /// One or more of the values have types not supported by the operator.
163    #[error("The '{op}' operator does not work on values of type '{unsupported}'.")]
164    #[diagnostic(code(nu::parser::operator_unsupported_type))]
165    OperatorUnsupportedType {
166        op: &'static str,
167        unsupported: Type,
168        #[label = "does not support '{unsupported}'"]
169        op_span: Span,
170        #[label("{unsupported}")]
171        unsupported_span: Span,
172        #[help]
173        help: Option<&'static str>,
174    },
175
176    /// The operator supports the types of both values, but not the specific combination of their types.
177    #[error("Types '{lhs}' and '{rhs}' are not compatible for the '{op}' operator.")]
178    #[diagnostic(code(nu::parser::operator_incompatible_types))]
179    OperatorIncompatibleTypes {
180        op: &'static str,
181        lhs: Type,
182        rhs: Type,
183        #[label = "does not operate between '{lhs}' and '{rhs}'"]
184        op_span: Span,
185        #[label("{lhs}")]
186        lhs_span: Span,
187        #[label("{rhs}")]
188        rhs_span: Span,
189        #[help]
190        help: Option<&'static str>,
191    },
192
193    #[error("Capture of mutable variable.")]
194    #[diagnostic(code(nu::parser::expected_keyword))]
195    CaptureOfMutableVar(#[label("capture of mutable variable")] Span),
196
197    #[error("Expected keyword.")]
198    #[diagnostic(code(nu::parser::expected_keyword))]
199    ExpectedKeyword(String, #[label("expected {0}")] Span),
200
201    #[error("Unexpected keyword.")]
202    #[diagnostic(
203        code(nu::parser::unexpected_keyword),
204        help("'{0}' keyword is allowed only in a module.")
205    )]
206    UnexpectedKeyword(String, #[label("unexpected {0}")] Span),
207
208    #[error("Module `{0}` has a `main` command but `{0}` is a built-in parser keyword.")]
209    #[diagnostic(
210        code(nu::parser::keyword_shadow_module_main),
211        help(
212            "The `main` command cannot be invoked because `{0}` is intercepted by the parser. Either rename the module file, or remove `export def main` and use `use {0}.nu *` to import other commands."
213        )
214    )]
215    KeywordShadowModuleMain(String, #[label("`{0}` is a parser keyword")] Span),
216
217    #[error("Can't create alias to parser keyword.")]
218    #[diagnostic(
219        code(nu::parser::cant_alias_keyword),
220        help("Only the following keywords can be aliased: {0}.")
221    )]
222    CantAliasKeyword(String, #[label("not supported in alias")] Span),
223
224    #[error("Can't create alias to expression.")]
225    #[diagnostic(
226        code(nu::parser::cant_alias_expression),
227        help("Only command calls can be aliased.")
228    )]
229    CantAliasExpression(String, #[label("aliasing {0} is not supported")] Span),
230
231    #[error("Unknown operator")]
232    #[diagnostic(code(nu::parser::unknown_operator), help("{1}"))]
233    UnknownOperator(
234        &'static str,
235        &'static str,
236        #[label("Operator '{0}' not supported")] Span,
237    ),
238
239    #[error("Statement used in pipeline.")]
240    #[diagnostic(
241        code(nu::parser::unexpected_keyword),
242        help(
243            "'{0}' keyword is not allowed in pipeline. Use '{0}' by itself, outside of a pipeline."
244        )
245    )]
246    BuiltinCommandInPipeline(String, #[label("not allowed in pipeline")] Span),
247
248    #[error("{0} statement used in pipeline.")]
249    #[diagnostic(
250        code(nu::parser::unexpected_keyword),
251        help(
252            "Assigning '{1}' to '{2}' does not produce a value to be piped. If the pipeline result is meant to be assigned to '{2}', use '{0} {2} = ({1} | ...)'."
253        )
254    )]
255    AssignInPipeline(String, String, String, #[label("'{0}' in pipeline")] Span),
256
257    #[error("`{0}` used as variable name.")]
258    #[diagnostic(
259        code(nu::parser::name_is_builtin_var),
260        help(
261            "'{0}' is the name of a builtin Nushell variable and cannot be used as a variable name"
262        )
263    )]
264    NameIsBuiltinVar(String, #[label("already a builtin variable")] Span),
265
266    #[error("Can't use parser keyword `{0}` as {1} name.")]
267    #[diagnostic(
268        code(nu::parser::name_is_keyword),
269        help(
270            "Parser keywords cannot be shadowed (including via module exports and `use *`). Choose a different {1} name so language constructs keep working."
271        )
272    )]
273    NameIsKeyword(String, String, #[label("'{0}' is a parser keyword")] Span),
274
275    #[error("Incorrect value")]
276    #[diagnostic(code(nu::parser::incorrect_value), help("{2}"))]
277    IncorrectValue(String, #[label("unexpected {0}")] Span, String),
278
279    #[error("Invalid binary string.")]
280    #[diagnostic(code(nu::parser::invalid_binary_string), help("{1}"))]
281    InvalidBinaryString(#[label("invalid binary string")] Span, String),
282
283    #[error("Multiple rest params.")]
284    #[diagnostic(code(nu::parser::multiple_rest_params))]
285    MultipleRestParams(#[label = "multiple rest params"] Span),
286
287    #[error("Variable not found.")]
288    #[diagnostic(code(nu::parser::variable_not_found))]
289    VariableNotFound(DidYouMean, #[label = "variable not found. {0}"] Span),
290
291    #[error("Use $env.{0} instead of ${0}.")]
292    #[diagnostic(code(nu::parser::env_var_not_var))]
293    EnvVarNotVar(String, #[label = "use $env.{0} instead of ${0}"] Span),
294
295    #[error("Variable name not supported.")]
296    #[diagnostic(code(nu::parser::variable_not_valid))]
297    VariableNotValid(#[label = "variable name can't contain spaces or quotes"] Span),
298
299    #[error("Alias name not supported.")]
300    #[diagnostic(code(nu::parser::variable_not_valid))]
301    AliasNotValid(
302        #[label = "alias name can't be a number, a filesize, or contain #, ^, or %"] Span,
303    ),
304
305    #[error("Command name not supported.")]
306    #[diagnostic(code(nu::parser::variable_not_valid))]
307    CommandDefNotValid(
308        #[label = "command name can't be a number, a filesize, or contain #, ^, or %"] Span,
309    ),
310
311    #[error("Module not found.")]
312    #[diagnostic(
313        code(nu::parser::module_not_found),
314        help(
315            "module files and their paths must be available before your script is run as parsing occurs before anything is evaluated"
316        )
317    )]
318    ModuleNotFound(#[label = "module {1} not found"] Span, String),
319
320    #[error("Missing mod.nu file.")]
321    #[diagnostic(
322        code(nu::parser::module_missing_mod_nu_file),
323        help(
324            "Directory {0} is missing a mod.nu file.\n\nWhen importing a directory as a Nushell module, it needs to contain a mod.nu file (can be empty). Alternatively, you can use .nu files in the directory as modules individually."
325        )
326    )]
327    ModuleMissingModNuFile(
328        String,
329        #[label = "module directory is missing a mod.nu file"] Span,
330    ),
331
332    #[error("Circular import.")]
333    #[diagnostic(code(nu::parser::circular_import), help("{0}"))]
334    CircularImport(String, #[label = "detected circular import"] Span),
335
336    #[error("Can't export {0} named same as the module.")]
337    #[diagnostic(
338        code(nu::parser::named_as_module),
339        help(
340            "Module {1} can't export {0} named the same as the module. Either change the module name, or export `{2}` {0}."
341        )
342    )]
343    NamedAsModule(
344        String,
345        String,
346        String,
347        #[label = "can't export from module {1}"] Span,
348    ),
349
350    #[error("Module already contains 'main' command.")]
351    #[diagnostic(
352        code(nu::parser::module_double_main),
353        help("Tried to add 'main' command to module '{0}' but it has already been added.")
354    )]
355    ModuleDoubleMain(
356        String,
357        #[label = "module '{0}' already contains 'main'"] Span,
358    ),
359
360    #[error("Can't export alias defined as 'main'.")]
361    #[diagnostic(
362        code(nu::parser::export_main_alias_not_allowed),
363        help(
364            "Exporting aliases as 'main' is not allowed. Either rename the alias or convert it to a custom command."
365        )
366    )]
367    ExportMainAliasNotAllowed(#[label = "can't export from module"] Span),
368
369    #[error("Active overlay not found.")]
370    #[diagnostic(code(nu::parser::active_overlay_not_found))]
371    ActiveOverlayNotFound(#[label = "not an active overlay"] Span),
372
373    #[error("Overlay prefix mismatch.")]
374    #[diagnostic(
375        code(nu::parser::overlay_prefix_mismatch),
376        help(
377            "Overlay {0} already exists {1} a prefix. To add it again, do it {1} the --prefix flag."
378        )
379    )]
380    OverlayPrefixMismatch(
381        String,
382        String,
383        #[label = "already exists {1} a prefix"] Span,
384    ),
385
386    #[error("Module or overlay not found.")]
387    #[diagnostic(
388        code(nu::parser::module_or_overlay_not_found),
389        help(
390            "Requires either an existing overlay, a module, or an import pattern defining a module."
391        )
392    )]
393    ModuleOrOverlayNotFound(#[label = "not a module or an overlay"] Span),
394
395    #[error("Cannot remove the last overlay.")]
396    #[diagnostic(
397        code(nu::parser::cant_remove_last_overlay),
398        help("At least one overlay must always be active.")
399    )]
400    CantRemoveLastOverlay(#[label = "this is the last overlay, can't remove it"] Span),
401
402    #[error("Cannot hide default overlay.")]
403    #[diagnostic(
404        code(nu::parser::cant_hide_default_overlay),
405        help("'{0}' is a default overlay. Default overlays cannot be hidden.")
406    )]
407    CantHideDefaultOverlay(String, #[label = "can't hide overlay"] Span),
408
409    #[error("Cannot add overlay.")]
410    #[diagnostic(code(nu::parser::cant_add_overlay_help), help("{0}"))]
411    CantAddOverlayHelp(String, #[label = "cannot add this overlay"] Span),
412
413    #[error("Duplicate command definition within a block.")]
414    #[diagnostic(code(nu::parser::duplicate_command_def))]
415    DuplicateCommandDef(#[label = "defined more than once"] Span),
416
417    #[error("Unknown command.")]
418    #[diagnostic(
419        code(nu::parser::unknown_command),
420        // TODO: actual suggestions like "Did you mean `foo`?"
421    )]
422    UnknownCommand(#[label = "unknown command"] Span),
423
424    #[error("Non-UTF8 string.")]
425    #[diagnostic(code(nu::parser::non_utf8))]
426    NonUtf8(#[label = "non-UTF8 string"] Span),
427
428    #[error("The `{0}` command doesn't have flag `{1}`.")]
429    #[diagnostic(code(nu::parser::unknown_flag), help("{3}"))]
430    UnknownFlag(String, String, #[label = "unknown flag"] Span, String),
431
432    #[error("Unknown type.")]
433    #[diagnostic(code(nu::parser::unknown_type))]
434    UnknownType(#[label = "unknown type"] Span),
435
436    #[error("Missing flag argument.")]
437    #[diagnostic(code(nu::parser::missing_flag_param))]
438    MissingFlagParam(String, #[label = "flag missing {0} argument"] Span),
439
440    #[error("Only the last flag in a short flag batch can take an argument.")]
441    #[diagnostic(code(nu::parser::only_last_flag_in_batch_can_take_arg))]
442    OnlyLastFlagInBatchCanTakeArg(#[label = "only the last flag can take args"] Span),
443
444    #[error("Missing required positional argument.")]
445    #[diagnostic(
446        code(nu::parser::missing_positional),
447        help("Usage: {2}. Use `--help` for more information.")
448    )]
449    MissingPositional(String, #[label("missing {0}")] Span, String),
450
451    #[error("Missing argument to `{1}`.")]
452    #[diagnostic(code(nu::parser::keyword_missing_arg))]
453    KeywordMissingArgument(
454        String,
455        String,
456        #[label("missing {0} value that follows {1}")] Span,
457    ),
458
459    #[error("Missing type.")]
460    #[diagnostic(code(nu::parser::missing_type))]
461    MissingType(#[label = "expected type"] Span),
462
463    #[error("Type mismatch.")]
464    #[diagnostic(code(nu::parser::type_mismatch))]
465    TypeMismatch(Type, Type, #[label("expected {0}, found {1}")] Span), // expected, found, span
466
467    #[error("Type mismatch.")]
468    #[diagnostic(code(nu::parser::type_mismatch_help), help("{3}"))]
469    TypeMismatchHelp(Type, Type, #[label("expected {0}, found {1}")] Span, String), // expected, found, span, help
470
471    #[error("Missing required flag.")]
472    #[diagnostic(code(nu::parser::missing_required_flag))]
473    MissingRequiredFlag(String, #[label("missing required flag {0}")] Span),
474
475    #[error("Incomplete math expression.")]
476    #[diagnostic(code(nu::parser::incomplete_math_expression))]
477    IncompleteMathExpression(#[label = "incomplete math expression"] Span),
478
479    #[error("Unknown state.")]
480    #[diagnostic(code(nu::parser::unknown_state))]
481    UnknownState(String, #[label("{0}")] Span),
482
483    #[error("Internal error.")]
484    #[diagnostic(code(nu::parser::unknown_state))]
485    InternalError(String, #[label("{0}")] Span),
486
487    #[error("Parser incomplete.")]
488    #[diagnostic(code(nu::parser::parser_incomplete))]
489    IncompleteParser(#[label = "parser support missing for this expression"] Span),
490
491    #[error("Rest parameter needs a name.")]
492    #[diagnostic(code(nu::parser::rest_needs_name))]
493    RestNeedsName(#[label = "needs a parameter name"] Span),
494
495    #[error("Parameter not correct type.")]
496    #[diagnostic(code(nu::parser::parameter_mismatch_type))]
497    ParameterMismatchType(
498        String,
499        String,
500        String,
501        #[label = "parameter {0} needs to be '{1}' instead of '{2}'"] Span,
502    ),
503
504    #[error("Default values should be constant expressions.")]
505    #[diagnostic(code(nu::parser::non_constant_default_value))]
506    NonConstantDefaultValue(#[label = "expected a constant value"] Span),
507
508    #[error("Extra columns.")]
509    #[diagnostic(code(nu::parser::extra_columns))]
510    ExtraColumns(
511        usize,
512        #[label("expected {0} column{}", if *.0 == 1 { "" } else { "s" })] Span,
513    ),
514
515    #[error("Missing columns.")]
516    #[diagnostic(code(nu::parser::missing_columns))]
517    MissingColumns(
518        usize,
519        #[label("expected {0} column{}", if *.0 == 1 { "" } else { "s" })] Span,
520    ),
521
522    #[error("{0}")]
523    #[diagnostic(code(nu::parser::assignment_mismatch))]
524    AssignmentMismatch(String, String, #[label("{1}")] Span),
525
526    #[error("Wrong import pattern structure.")]
527    #[diagnostic(code(nu::parser::wrong_import_pattern))]
528    WrongImportPattern(String, #[label = "{0}"] Span),
529
530    #[error("Export not found.")]
531    #[diagnostic(code(nu::parser::export_not_found))]
532    ExportNotFound(#[label = "could not find imports"] Span),
533
534    #[error("File not found")]
535    #[diagnostic(
536        code(nu::parser::sourced_file_not_found),
537        help("sourced files need to be available before your script is run")
538    )]
539    SourcedFileNotFound(String, #[label("File not found: {0}")] Span),
540
541    #[error("Script file is too large to load with `run`")]
542    #[diagnostic(
543        code(nu::parser::script_file_too_large),
544        help(
545            "The `run` command refuses files larger than {max_size} bytes at parse time (file is {size} bytes). Use a smaller script, or invoke large scripts with `nu path/to/script.nu`."
546        )
547    )]
548    ScriptFileTooLarge {
549        path: String,
550        size: u64,
551        max_size: u64,
552        #[label("file too large for `run`: {path}")]
553        span: Span,
554    },
555
556    #[error("Script file does not appear to be text")]
557    #[diagnostic(
558        code(nu::parser::script_file_not_text),
559        help(
560            "The `run` command only loads UTF-8 text scripts. Binary data (NUL bytes, invalid UTF-8, or dense control characters) is rejected."
561        )
562    )]
563    ScriptFileNotText {
564        path: String,
565        #[label("not a text script for `run`: {path}")]
566        span: Span,
567    },
568
569    #[error("File not found")]
570    #[diagnostic(
571        code(nu::parser::registered_file_not_found),
572        help("registered files need to be available before your script is run")
573    )]
574    RegisteredFileNotFound(String, #[label("File not found: {0}")] Span),
575
576    #[error("File not found")]
577    #[diagnostic(code(nu::parser::file_not_found))]
578    FileNotFound(String, #[label("File not found: {0}")] Span),
579
580    #[error("Plugin not found")]
581    #[diagnostic(
582        code(nu::parser::plugin_not_found),
583        help(
584            "plugins need to be added to the plugin registry file before your script is run (see `plugin add`)"
585        )
586    )]
587    PluginNotFound {
588        name: String,
589        #[label("Plugin not found: {name}")]
590        name_span: Span,
591        #[label("in this registry file")]
592        plugin_config_span: Option<Span>,
593    },
594
595    #[error("Invalid literal")] // <problem> in <entity>.
596    #[diagnostic()]
597    InvalidLiteral(String, String, #[label("{0} in {1}")] Span),
598
599    #[error("{0}")]
600    #[diagnostic()]
601    LabeledError(String, String, #[label("{1}")] Span),
602
603    #[error("{error}")]
604    #[diagnostic(help("{help}"))]
605    LabeledErrorWithHelp {
606        error: String,
607        label: String,
608        help: String,
609        #[label("{label}")]
610        span: Span,
611    },
612
613    #[error("Redirection can not be used with {0}.")]
614    #[diagnostic()]
615    RedirectingBuiltinCommand(
616        &'static str,
617        #[label("not allowed here")] Span,
618        #[label("...and here")] Option<Span>,
619    ),
620
621    #[error("This command does not have a ...rest parameter")]
622    #[diagnostic(
623        code(nu::parser::unexpected_spread_arg),
624        help(
625            "To spread arguments, the command needs to define a multi-positional parameter in its signature, such as ...rest"
626        )
627    )]
628    UnexpectedSpreadArg(String, #[label = "unexpected spread argument"] Span),
629
630    /// Invalid assignment left-hand side
631    ///
632    /// ## Resolution
633    ///
634    /// Assignment requires that you assign to a mutable variable or cell path.
635    #[error("Assignment to an immutable variable.")]
636    #[diagnostic(
637        code(nu::parser::assignment_requires_mutable_variable),
638        help("declare the variable with `mut`, or shadow it again with `let`")
639    )]
640    AssignmentRequiresMutableVar(#[label("needs to be a mutable variable")] Span),
641
642    /// Invalid assignment left-hand side
643    ///
644    /// ## Resolution
645    ///
646    /// Assignment requires that you assign to a variable or variable cell path.
647    #[error("Assignment operations require a variable.")]
648    #[diagnostic(
649        code(nu::parser::assignment_requires_variable),
650        help("try assigning to a variable or a cell path of a variable")
651    )]
652    AssignmentRequiresVar(#[label("needs to be a variable")] Span),
653
654    #[error("Attributes must be followed by a definition.")]
655    #[diagnostic(
656        code(nu::parser::attribute_requires_definition),
657        help("try following this line with a `def` or `extern` definition")
658    )]
659    AttributeRequiresDefinition(#[label("must be followed by a definition")] Span),
660}
661
662impl ParseError {
663    /// Span covering the first `len` bytes of `span` (clamped to `span.end`).
664    ///
665    /// Used when labeling a multi-byte construct's opening delimiter.
666    pub fn opener_span(span: Span, len: usize) -> Span {
667        Span::new(span.start, span.start.saturating_add(len).min(span.end))
668    }
669
670    /// Build an [`Unclosed`](ParseError::Unclosed) with default help text.
671    pub fn unclosed(delimiter: &'static str, open_span: Span, end_span: Span) -> Self {
672        Self::Unclosed(
673            delimiter,
674            open_span,
675            end_span,
676            default_unclosed_help(delimiter, None),
677        )
678    }
679
680    /// Build an [`Unclosed`](ParseError::Unclosed) with an optional structure hint
681    /// (e.g. `record field ls`, `def foo`). Empty/`None` uses generic help.
682    pub fn unclosed_with_hint(
683        delimiter: &'static str,
684        open_span: Span,
685        end_span: Span,
686        structure_hint: Option<&str>,
687    ) -> Self {
688        Self::Unclosed(
689            delimiter,
690            open_span,
691            end_span,
692            default_unclosed_help(delimiter, structure_hint),
693        )
694    }
695
696    /// Build an [`Unbalanced`](ParseError::Unbalanced) with default help text.
697    pub fn unbalanced(open: &'static str, close: &'static str, close_span: Span) -> Self {
698        Self::Unbalanced(
699            open,
700            close,
701            close_span,
702            default_unbalanced_help(open, close),
703        )
704    }
705
706    pub fn span(&self) -> Span {
707        match self {
708            ParseError::ExtraTokens(s) => *s,
709            ParseError::ExtraPositional(_, s) => *s,
710            ParseError::UnexpectedEof(_, s) => *s,
711            // Jump-to-error: prefer where the closer was expected (mid-file or EOF).
712            ParseError::Unclosed(_, _open, end, _) => *end,
713            ParseError::Unbalanced(_, _, close, _) => *close,
714            ParseError::Expected(_, s) => *s,
715            ParseError::ExpectedWithStringMsg(_, s) => *s,
716            ParseError::ExpectedWithDidYouMean(_, _, s) => *s,
717            ParseError::Mismatch(_, _, s) => *s,
718            ParseError::OperatorUnsupportedType { op_span, .. } => *op_span,
719            ParseError::OperatorIncompatibleTypes { op_span, .. } => *op_span,
720            ParseError::ExpectedKeyword(_, s) => *s,
721            ParseError::UnexpectedKeyword(_, s) => *s,
722            ParseError::CantAliasKeyword(_, s) => *s,
723            ParseError::CantAliasExpression(_, s) => *s,
724            ParseError::BuiltinCommandInPipeline(_, s) => *s,
725            ParseError::AssignInPipeline(_, _, _, s) => *s,
726            ParseError::NameIsBuiltinVar(_, s) => *s,
727            ParseError::NameIsKeyword(_, _, s) => *s,
728            ParseError::CaptureOfMutableVar(s) => *s,
729            ParseError::IncorrectValue(_, s, _) => *s,
730            ParseError::InvalidBinaryString(s, _) => *s,
731            ParseError::MultipleRestParams(s) => *s,
732            ParseError::VariableNotFound(_, s) => *s,
733            ParseError::EnvVarNotVar(_, s) => *s,
734            ParseError::VariableNotValid(s) => *s,
735            ParseError::AliasNotValid(s) => *s,
736            ParseError::CommandDefNotValid(s) => *s,
737            ParseError::ModuleNotFound(s, _) => *s,
738            ParseError::ModuleMissingModNuFile(_, s) => *s,
739            ParseError::NamedAsModule(_, _, _, s) => *s,
740            ParseError::ModuleDoubleMain(_, s) => *s,
741            ParseError::ExportMainAliasNotAllowed(s) => *s,
742            ParseError::CircularImport(_, s) => *s,
743            ParseError::ModuleOrOverlayNotFound(s) => *s,
744            ParseError::ActiveOverlayNotFound(s) => *s,
745            ParseError::OverlayPrefixMismatch(_, _, s) => *s,
746            ParseError::CantRemoveLastOverlay(s) => *s,
747            ParseError::CantHideDefaultOverlay(_, s) => *s,
748            ParseError::CantAddOverlayHelp(_, s) => *s,
749            ParseError::DuplicateCommandDef(s) => *s,
750            ParseError::UnknownCommand(s) => *s,
751            ParseError::NonUtf8(s) => *s,
752            ParseError::UnknownFlag(_, _, s, _) => *s,
753            ParseError::RequiredAfterOptional(_, s) => *s,
754            ParseError::UnknownType(s) => *s,
755            ParseError::MissingFlagParam(_, s) => *s,
756            ParseError::OnlyLastFlagInBatchCanTakeArg(s) => *s,
757            ParseError::MissingPositional(_, s, _) => *s,
758            ParseError::KeywordMissingArgument(_, _, s) => *s,
759            ParseError::MissingType(s) => *s,
760            ParseError::TypeMismatch(_, _, s) => *s,
761            ParseError::TypeMismatchHelp(_, _, s, _) => *s,
762            ParseError::InputMismatch(_, s) => *s,
763            ParseError::OutputMismatch(_, _, s) => *s,
764            ParseError::MissingRequiredFlag(_, s) => *s,
765            ParseError::IncompleteMathExpression(s) => *s,
766            ParseError::UnknownState(_, s) => *s,
767            ParseError::InternalError(_, s) => *s,
768            ParseError::IncompleteParser(s) => *s,
769            ParseError::RestNeedsName(s) => *s,
770            ParseError::ParameterMismatchType(_, _, _, s) => *s,
771            ParseError::NonConstantDefaultValue(s) => *s,
772            ParseError::ExtraColumns(_, s) => *s,
773            ParseError::MissingColumns(_, s) => *s,
774            ParseError::AssignmentMismatch(_, _, s) => *s,
775            ParseError::WrongImportPattern(_, s) => *s,
776            ParseError::ExportNotFound(s) => *s,
777            ParseError::SourcedFileNotFound(_, s) => *s,
778            ParseError::ScriptFileTooLarge { span, .. } => *span,
779            ParseError::ScriptFileNotText { span, .. } => *span,
780            ParseError::RegisteredFileNotFound(_, s) => *s,
781            ParseError::FileNotFound(_, s) => *s,
782            ParseError::PluginNotFound { name_span, .. } => *name_span,
783            ParseError::LabeledError(_, _, s) => *s,
784            ParseError::ShellAndAnd(s) => *s,
785            ParseError::ShellOrOr(s) => *s,
786            ParseError::ShellErrRedirect(s) => *s,
787            ParseError::ShellOutErrRedirect(s) => *s,
788            ParseError::MultipleRedirections(_, _, s) => *s,
789            ParseError::UnexpectedRedirection { span } => *span,
790            ParseError::UnknownOperator(_, _, s) => *s,
791            ParseError::InvalidLiteral(_, _, s) => *s,
792            ParseError::LabeledErrorWithHelp { span: s, .. } => *s,
793            ParseError::RedirectingBuiltinCommand(_, s, _) => *s,
794            ParseError::UnexpectedSpreadArg(_, s) => *s,
795            ParseError::ExtraTokensAfterClosingDelimiter(s) => *s,
796            ParseError::AssignmentRequiresVar(s) => *s,
797            ParseError::AssignmentRequiresMutableVar(s) => *s,
798            ParseError::AttributeRequiresDefinition(s) => *s,
799            ParseError::KeywordShadowModuleMain(_, s) => *s,
800        }
801    }
802}
803
804fn default_unclosed_help(delimiter: &str, structure_hint: Option<&str>) -> String {
805    match structure_hint {
806        Some(hint) if !hint.is_empty() => format!(
807            "Add a matching `{delimiter}` to close {hint}, or check that an earlier closer closed the wrong block."
808        ),
809        _ => format!(
810            "Add a matching `{delimiter}` to close this delimiter, or check that an earlier closer closed the wrong block."
811        ),
812    }
813}
814
815fn default_unbalanced_help(open: &str, close: &str) -> String {
816    format!(
817        "Remove this `{close}` if it is extra, or add a matching `{open}` earlier. If you closed a block too early, the real mistake may be above."
818    )
819}
820
821#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
822pub struct DidYouMean(Option<String>);
823
824fn did_you_mean_impl(possibilities_bytes: &[&[u8]], input_bytes: &[u8]) -> Option<String> {
825    let input = from_utf8(input_bytes).ok()?;
826    let possibilities = possibilities_bytes
827        .iter()
828        .map(|p| from_utf8(p))
829        .collect::<Result<Vec<&str>, Utf8Error>>()
830        .ok()?;
831    did_you_mean(&possibilities, input)
832}
833impl DidYouMean {
834    pub fn new(possibilities_bytes: &[&[u8]], input_bytes: &[u8]) -> DidYouMean {
835        DidYouMean(did_you_mean_impl(possibilities_bytes, input_bytes))
836    }
837}
838
839impl From<Option<String>> for DidYouMean {
840    fn from(value: Option<String>) -> Self {
841        Self(value)
842    }
843}
844
845impl Display for DidYouMean {
846    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
847        if let Some(suggestion) = &self.0 {
848            write!(f, "Did you mean '{suggestion}'?")
849        } else {
850            write!(f, "")
851        }
852    }
853}