forc_util/
lib.rs

1//! Utility items shared between forc crates.
2use annotate_snippets::{
3    renderer::{AnsiColor, Style},
4    Annotation, AnnotationType, Renderer, Slice, Snippet, SourceAnnotation,
5};
6use anyhow::{bail, Context, Result};
7use forc_tracing::{println_action_green, println_error, println_red_err, println_yellow_err};
8use std::{
9    collections::{hash_map, HashSet},
10    fmt::Display,
11    fs::File,
12    hash::{Hash, Hasher},
13    path::{Path, PathBuf},
14    process::Termination,
15    str,
16};
17use sway_core::language::parsed::TreeType;
18use sway_error::{
19    diagnostic::{Diagnostic, Issue, Label, LabelType, Level, ToDiagnostic},
20    error::CompileError,
21    warning::CompileWarning,
22};
23use sway_types::{LineCol, LineColRange, SourceEngine, Span};
24use sway_utils::constants;
25
26pub mod bytecode;
27pub mod fs_locking;
28pub mod restricted;
29
30#[macro_use]
31pub mod cli;
32
33pub use ansiterm;
34pub use paste;
35pub use regex::Regex;
36
37pub const DEFAULT_OUTPUT_DIRECTORY: &str = "out";
38pub const DEFAULT_ERROR_EXIT_CODE: u8 = 1;
39pub const DEFAULT_SUCCESS_EXIT_CODE: u8 = 0;
40
41/// A result type for forc operations. This shouldn't be returned from entry points, instead return
42/// `ForcCliResult` to exit with correct exit code.
43pub type ForcResult<T, E = ForcError> = Result<T, E>;
44
45/// A wrapper around `ForcResult`. Designed to be returned from entry points as it handles
46/// error reporting and exits with correct exit code.
47#[derive(Debug)]
48pub struct ForcCliResult<T> {
49    result: ForcResult<T>,
50}
51
52/// A forc error type which is a wrapper around `anyhow::Error`. It enables propagation of custom
53/// exit code alongisde the original error.
54#[derive(Debug)]
55pub struct ForcError {
56    error: anyhow::Error,
57    exit_code: u8,
58}
59
60impl ForcError {
61    pub fn new(error: anyhow::Error, exit_code: u8) -> Self {
62        Self { error, exit_code }
63    }
64
65    /// Returns a `ForcError` with provided exit_code.
66    pub fn exit_code(self, exit_code: u8) -> Self {
67        Self {
68            error: self.error,
69            exit_code,
70        }
71    }
72}
73
74impl AsRef<anyhow::Error> for ForcError {
75    fn as_ref(&self) -> &anyhow::Error {
76        &self.error
77    }
78}
79
80impl From<&str> for ForcError {
81    fn from(value: &str) -> Self {
82        Self {
83            error: anyhow::anyhow!("{value}"),
84            exit_code: DEFAULT_ERROR_EXIT_CODE,
85        }
86    }
87}
88
89impl From<anyhow::Error> for ForcError {
90    fn from(value: anyhow::Error) -> Self {
91        Self {
92            error: value,
93            exit_code: DEFAULT_ERROR_EXIT_CODE,
94        }
95    }
96}
97
98impl From<std::io::Error> for ForcError {
99    fn from(value: std::io::Error) -> Self {
100        Self {
101            error: value.into(),
102            exit_code: DEFAULT_ERROR_EXIT_CODE,
103        }
104    }
105}
106
107impl Display for ForcError {
108    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109        self.error.fmt(f)
110    }
111}
112
113impl<T> Termination for ForcCliResult<T> {
114    fn report(self) -> std::process::ExitCode {
115        match self.result {
116            Ok(_) => DEFAULT_SUCCESS_EXIT_CODE.into(),
117            Err(e) => {
118                println_error(&format!("{}", e));
119                e.exit_code.into()
120            }
121        }
122    }
123}
124
125impl<T> From<ForcResult<T>> for ForcCliResult<T> {
126    fn from(value: ForcResult<T>) -> Self {
127        Self { result: value }
128    }
129}
130
131#[macro_export]
132macro_rules! forc_result_bail {
133    ($msg:literal $(,)?) => {
134        return $crate::ForcResult::Err(anyhow::anyhow!($msg).into())
135    };
136    ($err:expr $(,)?) => {
137        return $crate::ForcResult::Err(anyhow::anyhow!($err).into())
138    };
139    ($fmt:expr, $($arg:tt)*) => {
140        return $crate::ForcResult::Err(anyhow::anyhow!($fmt, $($arg)*).into())
141    };
142}
143
144#[cfg(feature = "tx")]
145pub mod tx_utils {
146
147    use anyhow::Result;
148    use clap::Args;
149    use fuels_core::{codec::ABIDecoder, types::param_types::ParamType};
150    use serde::{Deserialize, Serialize};
151    use std::collections::HashMap;
152    use sway_core::{asm_generation::ProgramABI, fuel_prelude::fuel_tx};
153
154    /// Added salt used to derive the contract ID.
155    #[derive(Debug, Args, Default, Deserialize, Serialize)]
156    pub struct Salt {
157        /// Added salt used to derive the contract ID.
158        ///
159        /// By default, this is
160        /// `0x0000000000000000000000000000000000000000000000000000000000000000`.
161        #[clap(long = "salt")]
162        pub salt: Option<fuel_tx::Salt>,
163    }
164
165    /// Format `Log` and `LogData` receipts.
166    pub fn format_log_receipts(
167        receipts: &[fuel_tx::Receipt],
168        pretty_print: bool,
169    ) -> Result<String> {
170        let mut receipt_to_json_array = serde_json::to_value(receipts)?;
171        for (rec_index, receipt) in receipts.iter().enumerate() {
172            let rec_value = receipt_to_json_array.get_mut(rec_index).ok_or_else(|| {
173                anyhow::anyhow!(
174                    "Serialized receipts does not contain {} th index",
175                    rec_index
176                )
177            })?;
178            match receipt {
179                fuel_tx::Receipt::LogData {
180                    data: Some(data), ..
181                } => {
182                    if let Some(v) = rec_value.pointer_mut("/LogData/data") {
183                        *v = hex::encode(data).into();
184                    }
185                }
186                fuel_tx::Receipt::ReturnData {
187                    data: Some(data), ..
188                } => {
189                    if let Some(v) = rec_value.pointer_mut("/ReturnData/data") {
190                        *v = hex::encode(data).into();
191                    }
192                }
193                _ => {}
194            }
195        }
196        if pretty_print {
197            Ok(serde_json::to_string_pretty(&receipt_to_json_array)?)
198        } else {
199            Ok(serde_json::to_string(&receipt_to_json_array)?)
200        }
201    }
202
203    /// A `LogData` decoded into a human readable format with its type information.
204    pub struct DecodedLog {
205        pub value: String,
206    }
207
208    pub fn decode_log_data(
209        log_id: &str,
210        log_data: &[u8],
211        program_abi: &ProgramABI,
212    ) -> anyhow::Result<DecodedLog> {
213        let program_abi = match program_abi {
214            ProgramABI::Fuel(fuel_abi) => Some(
215                fuel_abi_types::abi::unified_program::UnifiedProgramABI::from_counterpart(
216                    fuel_abi,
217                )?,
218            ),
219            _ => None,
220        }
221        .ok_or_else(|| anyhow::anyhow!("only fuelvm is supported for log decoding"))?;
222        // Create type lookup (id, TypeDeclaration)
223        let type_lookup = program_abi
224            .types
225            .iter()
226            .map(|decl| (decl.type_id, decl.clone()))
227            .collect::<HashMap<_, _>>();
228
229        let logged_type_lookup: HashMap<_, _> = program_abi
230            .logged_types
231            .iter()
232            .flatten()
233            .map(|logged_type| (logged_type.log_id.as_str(), logged_type.application.clone()))
234            .collect();
235
236        let type_application = logged_type_lookup
237            .get(&log_id)
238            .ok_or_else(|| anyhow::anyhow!("log id is missing"))?;
239
240        let abi_decoder = ABIDecoder::default();
241        let param_type = ParamType::try_from_type_application(type_application, &type_lookup)?;
242        let decoded_str = abi_decoder.decode_as_debug_str(&param_type, log_data)?;
243        let decoded_log = DecodedLog { value: decoded_str };
244
245        Ok(decoded_log)
246    }
247}
248
249pub fn find_file_name<'sc>(manifest_dir: &Path, entry_path: &'sc Path) -> Result<&'sc Path> {
250    let mut file_path = manifest_dir.to_path_buf();
251    file_path.pop();
252    let file_name = match entry_path.strip_prefix(file_path.clone()) {
253        Ok(o) => o,
254        Err(err) => bail!(err),
255    };
256    Ok(file_name)
257}
258
259pub fn lock_path(manifest_dir: &Path) -> PathBuf {
260    manifest_dir.join(constants::LOCK_FILE_NAME)
261}
262
263pub fn validate_project_name(name: &str) -> Result<()> {
264    restricted::is_valid_project_name_format(name)?;
265    validate_name(name, "project name")
266}
267
268// Using (https://github.com/rust-lang/cargo/blob/489b66f2e458404a10d7824194d3ded94bc1f4e4/src/cargo/util/toml/mod.rs +
269// https://github.com/rust-lang/cargo/blob/489b66f2e458404a10d7824194d3ded94bc1f4e4/src/cargo/ops/cargo_new.rs) for reference
270
271pub fn validate_name(name: &str, use_case: &str) -> Result<()> {
272    // if true returns formatted error
273    restricted::contains_invalid_char(name, use_case)?;
274
275    if restricted::is_keyword(name) {
276        bail!("the name `{name}` cannot be used as a {use_case}, it is a Sway keyword");
277    }
278    if restricted::is_conflicting_artifact_name(name) {
279        bail!(
280            "the name `{name}` cannot be used as a {use_case}, \
281            it conflicts with Forc's build directory names"
282        );
283    }
284    if name.to_lowercase() == "test" {
285        bail!(
286            "the name `test` cannot be used as a {use_case}, \
287            it conflicts with Sway's built-in test library"
288        );
289    }
290    if restricted::is_conflicting_suffix(name) {
291        bail!(
292            "the name `{name}` is part of Sway's standard library\n\
293            It is recommended to use a different name to avoid problems."
294        );
295    }
296    if restricted::is_windows_reserved(name) {
297        if cfg!(windows) {
298            bail!("cannot use name `{name}`, it is a reserved Windows filename");
299        } else {
300            bail!(
301                "the name `{name}` is a reserved Windows filename\n\
302                This package will not work on Windows platforms."
303            );
304        }
305    }
306    if restricted::is_non_ascii_name(name) {
307        bail!("the name `{name}` contains non-ASCII characters which are unsupported");
308    }
309    Ok(())
310}
311
312/// Simple function to convert kebab-case to snake_case.
313pub fn kebab_to_snake_case(s: &str) -> String {
314    s.replace('-', "_")
315}
316
317pub fn default_output_directory(manifest_dir: &Path) -> PathBuf {
318    manifest_dir.join(DEFAULT_OUTPUT_DIRECTORY)
319}
320
321/// Returns the user's `.forc` directory, `$HOME/.forc` by default.
322pub fn user_forc_directory() -> PathBuf {
323    dirs::home_dir()
324        .expect("unable to find the user home directory")
325        .join(constants::USER_FORC_DIRECTORY)
326}
327
328/// The location at which `forc` will checkout git repositories.
329pub fn git_checkouts_directory() -> PathBuf {
330    user_forc_directory().join("git").join("checkouts")
331}
332
333/// Given a path to a directory we wish to lock, produce a path for an associated lock file.
334///
335/// Note that the lock file itself is simply a placeholder for co-ordinating access. As a result,
336/// we want to create the lock file if it doesn't exist, but we can never reliably remove it
337/// without risking invalidation of an existing lock. As a result, we use a dedicated, hidden
338/// directory with a lock file named after the checkout path.
339///
340/// Note: This has nothing to do with `Forc.lock` files, rather this is about fd locks for
341/// coordinating access to particular paths (e.g. git checkout directories).
342fn fd_lock_path<X: AsRef<Path>>(path: X) -> PathBuf {
343    const LOCKS_DIR_NAME: &str = ".locks";
344    const LOCK_EXT: &str = "forc-lock";
345    let file_name = hash_path(path);
346    user_forc_directory()
347        .join(LOCKS_DIR_NAME)
348        .join(file_name)
349        .with_extension(LOCK_EXT)
350}
351
352/// Hash the path to produce a file-system friendly file name.
353/// Append the file stem for improved readability.
354fn hash_path<X: AsRef<Path>>(path: X) -> String {
355    let path = path.as_ref();
356    let mut hasher = hash_map::DefaultHasher::default();
357    path.hash(&mut hasher);
358    let hash = hasher.finish();
359    let file_name = match path.file_stem().and_then(|s| s.to_str()) {
360        None => format!("{hash:X}"),
361        Some(stem) => format!("{hash:X}-{stem}"),
362    };
363    file_name
364}
365
366/// Create an advisory lock over the given path.
367///
368/// See [fd_lock_path] for details.
369pub fn path_lock<X: AsRef<Path>>(path: X) -> Result<fd_lock::RwLock<File>> {
370    let lock_path = fd_lock_path(path);
371    let lock_dir = lock_path
372        .parent()
373        .expect("lock path has no parent directory");
374    std::fs::create_dir_all(lock_dir).context("failed to create forc advisory lock directory")?;
375    let lock_file = File::create(&lock_path).context("failed to create advisory lock file")?;
376    Ok(fd_lock::RwLock::new(lock_file))
377}
378
379pub fn program_type_str(ty: &TreeType) -> &'static str {
380    match ty {
381        TreeType::Script => "script",
382        TreeType::Contract => "contract",
383        TreeType::Predicate => "predicate",
384        TreeType::Library => "library",
385    }
386}
387
388pub fn print_compiling(ty: Option<&TreeType>, name: &str, src: &dyn std::fmt::Display) {
389    // NOTE: We can only print the program type if we can parse the program, so
390    // program type must be optional.
391    let ty = match ty {
392        Some(ty) => format!("{} ", program_type_str(ty)),
393        None => "".to_string(),
394    };
395    println_action_green(
396        "Compiling",
397        &format!("{ty}{} ({src})", ansiterm::Style::new().bold().paint(name)),
398    );
399}
400
401pub fn print_warnings(
402    source_engine: &SourceEngine,
403    terse_mode: bool,
404    proj_name: &str,
405    warnings: &[CompileWarning],
406    tree_type: &TreeType,
407) {
408    if warnings.is_empty() {
409        return;
410    }
411    let type_str = program_type_str(tree_type);
412
413    if !terse_mode {
414        warnings
415            .iter()
416            .for_each(|w| format_diagnostic(&w.to_diagnostic(source_engine)));
417    }
418
419    println_yellow_err(&format!(
420        "  Compiled {} {:?} with {} {}.",
421        type_str,
422        proj_name,
423        warnings.len(),
424        if warnings.len() > 1 {
425            "warnings"
426        } else {
427            "warning"
428        }
429    ));
430}
431
432pub fn print_on_failure(
433    source_engine: &SourceEngine,
434    terse_mode: bool,
435    warnings: &[CompileWarning],
436    errors: &[CompileError],
437    reverse_results: bool,
438) {
439    let e_len = errors.len();
440    let w_len = warnings.len();
441
442    if !terse_mode {
443        if reverse_results {
444            warnings
445                .iter()
446                .rev()
447                .for_each(|w| format_diagnostic(&w.to_diagnostic(source_engine)));
448            errors
449                .iter()
450                .rev()
451                .for_each(|e| format_diagnostic(&e.to_diagnostic(source_engine)));
452        } else {
453            warnings
454                .iter()
455                .for_each(|w| format_diagnostic(&w.to_diagnostic(source_engine)));
456            errors
457                .iter()
458                .for_each(|e| format_diagnostic(&e.to_diagnostic(source_engine)));
459        }
460    }
461
462    if e_len == 0 && w_len > 0 {
463        println_red_err(&format!(
464            "  Aborting. {} warning(s) treated as error(s).",
465            warnings.len()
466        ));
467    } else {
468        println_red_err(&format!(
469            "  Aborting due to {} {}.",
470            e_len,
471            if e_len > 1 { "errors" } else { "error" }
472        ));
473    }
474}
475
476/// Creates [Renderer] for printing warnings and errors.
477///
478/// To ensure the same styling of printed warnings and errors across all the tools,
479/// always use this function to create [Renderer]s,
480pub fn create_diagnostics_renderer() -> Renderer {
481    // For the diagnostic messages we use bold and bright colors.
482    // Note that for the summaries of warnings and errors we use
483    // their regular equivalents which are defined in `forc-tracing` package.
484    Renderer::styled()
485        .warning(
486            Style::new()
487                .bold()
488                .fg_color(Some(AnsiColor::BrightYellow.into())),
489        )
490        .error(
491            Style::new()
492                .bold()
493                .fg_color(Some(AnsiColor::BrightRed.into())),
494        )
495}
496
497pub fn format_diagnostic(diagnostic: &Diagnostic) {
498    /// Temporary switch for testing the feature.
499    /// Keep it false until we decide to fully support the diagnostic codes.
500    const SHOW_DIAGNOSTIC_CODE: bool = false;
501
502    if diagnostic.is_old_style() {
503        format_old_style_diagnostic(diagnostic.issue());
504        return;
505    }
506
507    let mut label = String::new();
508    get_title_label(diagnostic, &mut label);
509
510    let snippet_title = Some(Annotation {
511        label: Some(label.as_str()),
512        id: if SHOW_DIAGNOSTIC_CODE {
513            diagnostic.reason().map(|reason| reason.code())
514        } else {
515            None
516        },
517        annotation_type: diagnostic_level_to_annotation_type(diagnostic.level()),
518    });
519
520    let mut snippet_slices = Vec::<Slice<'_>>::new();
521
522    // We first display labels from the issue file...
523    if diagnostic.issue().is_in_source() {
524        snippet_slices.push(construct_slice(diagnostic.labels_in_issue_source()))
525    }
526
527    // ...and then all the remaining labels from the other files.
528    for source_path in diagnostic.related_sources(false) {
529        snippet_slices.push(construct_slice(diagnostic.labels_in_source(source_path)))
530    }
531
532    let mut snippet_footer = Vec::<Annotation<'_>>::new();
533    for help in diagnostic.help() {
534        snippet_footer.push(Annotation {
535            id: None,
536            label: Some(help),
537            annotation_type: AnnotationType::Help,
538        });
539    }
540
541    let snippet = Snippet {
542        title: snippet_title,
543        slices: snippet_slices,
544        footer: snippet_footer,
545    };
546
547    let renderer = create_diagnostics_renderer();
548    match diagnostic.level() {
549        Level::Info => tracing::info!("{}\n____\n", renderer.render(snippet)),
550        Level::Warning => tracing::warn!("{}\n____\n", renderer.render(snippet)),
551        Level::Error => tracing::error!("{}\n____\n", renderer.render(snippet)),
552    }
553
554    fn format_old_style_diagnostic(issue: &Issue) {
555        let annotation_type = label_type_to_annotation_type(issue.label_type());
556
557        let snippet_title = Some(Annotation {
558            label: if issue.is_in_source() {
559                None
560            } else {
561                Some(issue.text())
562            },
563            id: None,
564            annotation_type,
565        });
566
567        let mut snippet_slices = vec![];
568        if issue.is_in_source() {
569            let span = issue.span();
570            let input = span.input();
571            let mut start_pos = span.start();
572            let mut end_pos = span.end();
573            let LineColRange { mut start, end } = span.line_col_one_index();
574            let input = construct_window(&mut start, end, &mut start_pos, &mut end_pos, input);
575
576            let slice = Slice {
577                source: input,
578                line_start: start.line,
579                // Safe unwrap because the issue is in source, so the source path surely exists.
580                origin: Some(issue.source_path().unwrap().as_str()),
581                fold: false,
582                annotations: vec![SourceAnnotation {
583                    label: issue.text(),
584                    annotation_type,
585                    range: (start_pos, end_pos),
586                }],
587            };
588
589            snippet_slices.push(slice);
590        }
591
592        let snippet = Snippet {
593            title: snippet_title,
594            footer: vec![],
595            slices: snippet_slices,
596        };
597
598        let renderer = create_diagnostics_renderer();
599        tracing::error!("{}\n____\n", renderer.render(snippet));
600    }
601
602    fn get_title_label(diagnostics: &Diagnostic, label: &mut String) {
603        label.clear();
604        if let Some(reason) = diagnostics.reason() {
605            label.push_str(reason.description());
606        }
607    }
608
609    fn diagnostic_level_to_annotation_type(level: Level) -> AnnotationType {
610        match level {
611            Level::Info => AnnotationType::Info,
612            Level::Warning => AnnotationType::Warning,
613            Level::Error => AnnotationType::Error,
614        }
615    }
616}
617
618fn construct_slice(labels: Vec<&Label>) -> Slice {
619    debug_assert!(
620        !labels.is_empty(),
621        "To construct slices, at least one label must be provided."
622    );
623
624    debug_assert!(
625        labels.iter().all(|label| label.is_in_source()),
626        "Slices can be constructed only for labels that are related to a place in source code."
627    );
628
629    debug_assert!(
630        HashSet::<&str>::from_iter(labels.iter().map(|label| label.source_path().unwrap().as_str())).len() == 1,
631        "Slices can be constructed only for labels that are related to places in the same source code."
632    );
633
634    let source_file = labels[0].source_path().map(|path| path.as_str());
635    let source_code = labels[0].span().input();
636
637    // Joint span of the code snippet that covers all the labels.
638    let span = Span::join_all(labels.iter().map(|label| label.span().clone()));
639
640    let (source, line_start, shift_in_bytes) = construct_code_snippet(&span, source_code);
641
642    let mut annotations = vec![];
643
644    for message in labels {
645        annotations.push(SourceAnnotation {
646            label: message.text(),
647            annotation_type: label_type_to_annotation_type(message.label_type()),
648            range: get_annotation_range(message.span(), source_code, shift_in_bytes),
649        });
650    }
651
652    return Slice {
653        source,
654        line_start,
655        origin: source_file,
656        fold: true,
657        annotations,
658    };
659
660    fn get_annotation_range(
661        span: &Span,
662        source_code: &str,
663        shift_in_bytes: usize,
664    ) -> (usize, usize) {
665        let mut start_pos = span.start();
666        let mut end_pos = span.end();
667
668        let start_ix_bytes = start_pos - std::cmp::min(shift_in_bytes, start_pos);
669        let end_ix_bytes = end_pos - std::cmp::min(shift_in_bytes, end_pos);
670
671        // We want the start_pos and end_pos in terms of chars and not bytes, so translate.
672        start_pos = source_code[shift_in_bytes..(shift_in_bytes + start_ix_bytes)]
673            .chars()
674            .count();
675        end_pos = source_code[shift_in_bytes..(shift_in_bytes + end_ix_bytes)]
676            .chars()
677            .count();
678
679        (start_pos, end_pos)
680    }
681}
682
683fn label_type_to_annotation_type(label_type: LabelType) -> AnnotationType {
684    match label_type {
685        LabelType::Info => AnnotationType::Info,
686        LabelType::Help => AnnotationType::Help,
687        LabelType::Warning => AnnotationType::Warning,
688        LabelType::Error => AnnotationType::Error,
689    }
690}
691
692/// Given the overall span to be shown in the code snippet, determines how much of the input source
693/// to show in the snippet.
694///
695/// Returns the source to be shown, the line start, and the offset of the snippet in bytes relative
696/// to the beginning of the input code.
697///
698/// The library we use doesn't handle auto-windowing and line numbers, so we must manually
699/// calculate the line numbers and match them up with the input window. It is a bit fiddly.
700fn construct_code_snippet<'a>(span: &Span, input: &'a str) -> (&'a str, usize, usize) {
701    // how many lines to prepend or append to the highlighted region in the window
702    const NUM_LINES_BUFFER: usize = 2;
703
704    let LineColRange { start, end } = span.line_col_one_index();
705
706    let total_lines_in_input = input.chars().filter(|x| *x == '\n').count();
707    debug_assert!(end.line >= start.line);
708    let total_lines_of_highlight = end.line - start.line;
709    debug_assert!(total_lines_in_input >= total_lines_of_highlight);
710
711    let mut current_line = 0;
712    let mut lines_to_start_of_snippet = 0;
713    let mut calculated_start_ix = None;
714    let mut calculated_end_ix = None;
715    let mut pos = 0;
716    for character in input.chars() {
717        if character == '\n' {
718            current_line += 1
719        }
720
721        if current_line + NUM_LINES_BUFFER >= start.line && calculated_start_ix.is_none() {
722            calculated_start_ix = Some(pos);
723            lines_to_start_of_snippet = current_line;
724        }
725
726        if current_line >= end.line + NUM_LINES_BUFFER && calculated_end_ix.is_none() {
727            calculated_end_ix = Some(pos);
728        }
729
730        if calculated_start_ix.is_some() && calculated_end_ix.is_some() {
731            break;
732        }
733        pos += character.len_utf8();
734    }
735    let calculated_start_ix = calculated_start_ix.unwrap_or(0);
736    let calculated_end_ix = calculated_end_ix.unwrap_or(input.len());
737
738    (
739        &input[calculated_start_ix..calculated_end_ix],
740        lines_to_start_of_snippet,
741        calculated_start_ix,
742    )
743}
744
745// TODO: Remove once "old-style" diagnostic is fully replaced with new one and the backward
746//       compatibility is no longer needed.
747/// Given a start and an end position and an input, determine how much of a window to show in the
748/// error.
749/// Mutates the start and end indexes to be in line with the new slice length.
750///
751/// The library we use doesn't handle auto-windowing and line numbers, so we must manually
752/// calculate the line numbers and match them up with the input window. It is a bit fiddly.
753fn construct_window<'a>(
754    start: &mut LineCol,
755    end: LineCol,
756    start_ix: &mut usize,
757    end_ix: &mut usize,
758    input: &'a str,
759) -> &'a str {
760    // how many lines to prepend or append to the highlighted region in the window
761    const NUM_LINES_BUFFER: usize = 2;
762
763    let total_lines_in_input = input.chars().filter(|x| *x == '\n').count();
764    debug_assert!(end.line >= start.line);
765    let total_lines_of_highlight = end.line - start.line;
766    debug_assert!(total_lines_in_input >= total_lines_of_highlight);
767
768    let mut current_line = 1usize;
769
770    let mut chars = input.char_indices().map(|(char_offset, character)| {
771        let r = (current_line, char_offset);
772        if character == '\n' {
773            current_line += 1;
774        }
775        r
776    });
777
778    // Find the first char of the first line
779    let first_char = chars
780        .by_ref()
781        .find(|(current_line, _)| current_line + NUM_LINES_BUFFER >= start.line);
782
783    // Find the last char of the last line
784    let last_char = chars
785        .by_ref()
786        .find(|(current_line, _)| *current_line > end.line + NUM_LINES_BUFFER)
787        .map(|x| x.1);
788
789    // this releases the borrow of `current_line`
790    drop(chars);
791
792    let (first_char_line, first_char_offset, last_char_offset) = match (first_char, last_char) {
793        // has first and last
794        (Some((first_char_line, first_char_offset)), Some(last_char_offset)) => {
795            (first_char_line, first_char_offset, last_char_offset)
796        }
797        // has first and no last
798        (Some((first_char_line, first_char_offset)), None) => {
799            (first_char_line, first_char_offset, input.len())
800        }
801        // others
802        _ => (current_line, input.len(), input.len()),
803    };
804
805    // adjust indices to be inside the returned window
806    start.line = first_char_line;
807    *start_ix = start_ix.saturating_sub(first_char_offset);
808    *end_ix = end_ix.saturating_sub(first_char_offset);
809
810    &input[first_char_offset..last_char_offset]
811}
812
813#[test]
814fn ok_construct_window() {
815    fn t(
816        start_line: usize,
817        start_col: usize,
818        end_line: usize,
819        end_col: usize,
820        start_char: usize,
821        end_char: usize,
822        input: &str,
823    ) -> (usize, usize, &str) {
824        let mut s = LineCol {
825            line: start_line,
826            col: start_col,
827        };
828        let mut start = start_char;
829        let mut end = end_char;
830        let r = construct_window(
831            &mut s,
832            LineCol {
833                line: end_line,
834                col: end_col,
835            },
836            &mut start,
837            &mut end,
838            input,
839        );
840        (start, end, r)
841    }
842
843    // Invalid Empty file
844    assert_eq!(t(0, 0, 0, 0, 0, 0, ""), (0, 0, ""));
845
846    // Valid Empty File
847    assert_eq!(t(1, 1, 1, 1, 0, 0, ""), (0, 0, ""));
848
849    // One line, error after the last char
850    assert_eq!(t(1, 7, 1, 7, 6, 6, "script"), (6, 6, "script"));
851
852    //                       01 23 45 67 89 AB CD E
853    let eight_lines = "1\n2\n3\n4\n5\n6\n7\n8";
854
855    assert_eq!(t(1, 1, 1, 1, 0, 1, eight_lines), (0, 1, "1\n2\n3\n"));
856    assert_eq!(t(2, 1, 2, 1, 2, 3, eight_lines), (2, 3, "1\n2\n3\n4\n"));
857    assert_eq!(t(3, 1, 3, 1, 4, 5, eight_lines), (4, 5, "1\n2\n3\n4\n5\n"));
858    assert_eq!(t(4, 1, 4, 1, 6, 7, eight_lines), (4, 5, "2\n3\n4\n5\n6\n"));
859    assert_eq!(t(5, 1, 5, 1, 8, 9, eight_lines), (4, 5, "3\n4\n5\n6\n7\n"));
860    assert_eq!(t(6, 1, 6, 1, 10, 11, eight_lines), (4, 5, "4\n5\n6\n7\n8"));
861    assert_eq!(t(7, 1, 7, 1, 12, 13, eight_lines), (4, 5, "5\n6\n7\n8"));
862    assert_eq!(t(8, 1, 8, 1, 14, 15, eight_lines), (4, 5, "6\n7\n8"));
863
864    // Invalid lines
865    assert_eq!(t(9, 1, 9, 1, 14, 15, eight_lines), (2, 3, "7\n8"));
866    assert_eq!(t(10, 1, 10, 1, 14, 15, eight_lines), (0, 1, "8"));
867    assert_eq!(t(11, 1, 11, 1, 14, 15, eight_lines), (0, 0, ""));
868}