1use 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::{CompileInfo, 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#[cfg(feature = "tx")]
30pub mod tx_utils;
31
32#[macro_use]
33pub mod cli;
34
35pub use ansiterm;
36pub use paste;
37pub use regex::Regex;
38
39pub const DEFAULT_OUTPUT_DIRECTORY: &str = "out";
40pub const DEFAULT_ERROR_EXIT_CODE: u8 = 1;
41pub const DEFAULT_SUCCESS_EXIT_CODE: u8 = 0;
42
43pub type ForcResult<T, E = ForcError> = Result<T, E>;
46
47#[derive(Debug)]
50pub struct ForcCliResult<T> {
51 result: ForcResult<T>,
52}
53
54#[derive(Debug)]
57pub struct ForcError {
58 error: anyhow::Error,
59 exit_code: u8,
60}
61
62impl ForcError {
63 pub fn new(error: anyhow::Error, exit_code: u8) -> Self {
64 Self { error, exit_code }
65 }
66
67 pub fn exit_code(self, exit_code: u8) -> Self {
69 Self {
70 error: self.error,
71 exit_code,
72 }
73 }
74}
75
76impl AsRef<anyhow::Error> for ForcError {
77 fn as_ref(&self) -> &anyhow::Error {
78 &self.error
79 }
80}
81
82impl From<&str> for ForcError {
83 fn from(value: &str) -> Self {
84 Self {
85 error: anyhow::anyhow!("{value}"),
86 exit_code: DEFAULT_ERROR_EXIT_CODE,
87 }
88 }
89}
90
91impl From<anyhow::Error> for ForcError {
92 fn from(value: anyhow::Error) -> Self {
93 Self {
94 error: value,
95 exit_code: DEFAULT_ERROR_EXIT_CODE,
96 }
97 }
98}
99
100impl From<std::io::Error> for ForcError {
101 fn from(value: std::io::Error) -> Self {
102 Self {
103 error: value.into(),
104 exit_code: DEFAULT_ERROR_EXIT_CODE,
105 }
106 }
107}
108
109impl Display for ForcError {
110 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111 self.error.fmt(f)
112 }
113}
114
115impl<T> Termination for ForcCliResult<T> {
116 fn report(self) -> std::process::ExitCode {
117 match self.result {
118 Ok(_) => DEFAULT_SUCCESS_EXIT_CODE.into(),
119 Err(e) => {
120 println_error(&format!("{e}"));
121 e.exit_code.into()
122 }
123 }
124 }
125}
126
127impl<T> From<ForcResult<T>> for ForcCliResult<T> {
128 fn from(value: ForcResult<T>) -> Self {
129 Self { result: value }
130 }
131}
132
133#[macro_export]
134macro_rules! forc_result_bail {
135 ($msg:literal $(,)?) => {
136 return $crate::ForcResult::Err(anyhow::anyhow!($msg).into())
137 };
138 ($err:expr $(,)?) => {
139 return $crate::ForcResult::Err(anyhow::anyhow!($err).into())
140 };
141 ($fmt:expr, $($arg:tt)*) => {
142 return $crate::ForcResult::Err(anyhow::anyhow!($fmt, $($arg)*).into())
143 };
144}
145
146pub fn find_file_name<'sc>(manifest_dir: &Path, entry_path: &'sc Path) -> Result<&'sc Path> {
147 let mut file_path = manifest_dir.to_path_buf();
148 file_path.pop();
149 let file_name = match entry_path.strip_prefix(file_path.clone()) {
150 Ok(o) => o,
151 Err(err) => bail!(err),
152 };
153 Ok(file_name)
154}
155
156pub fn lock_path(manifest_dir: &Path) -> PathBuf {
157 manifest_dir.join(constants::LOCK_FILE_NAME)
158}
159
160pub fn validate_project_name(name: &str) -> Result<()> {
161 restricted::is_valid_project_name_format(name)?;
162 validate_name(name, "project name")
163}
164
165pub fn validate_name(name: &str, use_case: &str) -> Result<()> {
169 restricted::contains_invalid_char(name, use_case)?;
171
172 if restricted::is_keyword(name) {
173 bail!("the name `{name}` cannot be used as a {use_case}, it is a Sway keyword");
174 }
175 if restricted::is_conflicting_artifact_name(name) {
176 bail!(
177 "the name `{name}` cannot be used as a {use_case}, \
178 it conflicts with Forc's build directory names"
179 );
180 }
181 if name.to_lowercase() == "test" {
182 bail!(
183 "the name `test` cannot be used as a {use_case}, \
184 it conflicts with Sway's built-in test library"
185 );
186 }
187 if restricted::is_conflicting_suffix(name) {
188 bail!(
189 "the name `{name}` is part of Sway's standard library\n\
190 It is recommended to use a different name to avoid problems."
191 );
192 }
193 if restricted::is_windows_reserved(name) {
194 if cfg!(windows) {
195 bail!("cannot use name `{name}`, it is a reserved Windows filename");
196 } else {
197 bail!(
198 "the name `{name}` is a reserved Windows filename\n\
199 This package will not work on Windows platforms."
200 );
201 }
202 }
203 if restricted::is_non_ascii_name(name) {
204 bail!("the name `{name}` contains non-ASCII characters which are unsupported");
205 }
206 Ok(())
207}
208
209pub fn kebab_to_snake_case(s: &str) -> String {
211 s.replace('-', "_")
212}
213
214pub fn default_output_directory(manifest_dir: &Path) -> PathBuf {
215 manifest_dir.join(DEFAULT_OUTPUT_DIRECTORY)
216}
217
218pub fn user_forc_directory() -> PathBuf {
220 dirs::home_dir()
221 .expect("unable to find the user home directory")
222 .join(constants::USER_FORC_DIRECTORY)
223}
224
225pub fn git_checkouts_directory() -> PathBuf {
227 user_forc_directory().join("git").join("checkouts")
228}
229
230fn fd_lock_path<X: AsRef<Path>>(path: X) -> PathBuf {
240 const LOCKS_DIR_NAME: &str = ".locks";
241 const LOCK_EXT: &str = "forc-lock";
242 let file_name = hash_path(path);
243 user_forc_directory()
244 .join(LOCKS_DIR_NAME)
245 .join(file_name)
246 .with_extension(LOCK_EXT)
247}
248
249fn hash_path<X: AsRef<Path>>(path: X) -> String {
252 let path = path.as_ref();
253 let mut hasher = hash_map::DefaultHasher::default();
254 path.hash(&mut hasher);
255 let hash = hasher.finish();
256 let file_name = match path.file_stem().and_then(|s| s.to_str()) {
257 None => format!("{hash:X}"),
258 Some(stem) => format!("{hash:X}-{stem}"),
259 };
260 file_name
261}
262
263pub fn path_lock<X: AsRef<Path>>(path: X) -> Result<fd_lock::RwLock<File>> {
267 let lock_path = fd_lock_path(path);
268 let lock_dir = lock_path
269 .parent()
270 .expect("lock path has no parent directory");
271 std::fs::create_dir_all(lock_dir).context("failed to create forc advisory lock directory")?;
272 let lock_file = File::create(&lock_path).context("failed to create advisory lock file")?;
273 Ok(fd_lock::RwLock::new(lock_file))
274}
275
276pub fn program_type_str(ty: &TreeType) -> &'static str {
277 match ty {
278 TreeType::Script => "script",
279 TreeType::Contract => "contract",
280 TreeType::Predicate => "predicate",
281 TreeType::Library => "library",
282 }
283}
284
285pub fn print_compiling(ty: Option<&TreeType>, name: &str, src: &dyn std::fmt::Display) {
286 let ty = match ty {
289 Some(ty) => format!("{} ", program_type_str(ty)),
290 None => "".to_string(),
291 };
292 println_action_green(
293 "Compiling",
294 &format!("{ty}{} ({src})", ansiterm::Style::new().bold().paint(name)),
295 );
296}
297
298pub fn print_infos(source_engine: &SourceEngine, terse_mode: bool, infos: &[CompileInfo]) {
299 if infos.is_empty() {
300 return;
301 }
302
303 if !terse_mode {
304 infos
305 .iter()
306 .for_each(|n| format_diagnostic(&n.to_diagnostic(source_engine)));
307 }
308}
309
310pub fn print_warnings(
311 source_engine: &SourceEngine,
312 terse_mode: bool,
313 proj_name: &str,
314 warnings: &[CompileWarning],
315 tree_type: &TreeType,
316) {
317 if warnings.is_empty() {
318 return;
319 }
320 let type_str = program_type_str(tree_type);
321
322 if !terse_mode {
323 warnings
324 .iter()
325 .for_each(|w| format_diagnostic(&w.to_diagnostic(source_engine)));
326 }
327
328 println_yellow_err(&format!(
329 " Compiled {} {:?} with {} {}.",
330 type_str,
331 proj_name,
332 warnings.len(),
333 if warnings.len() > 1 {
334 "warnings"
335 } else {
336 "warning"
337 }
338 ));
339}
340
341pub fn print_on_failure(
342 source_engine: &SourceEngine,
343 terse_mode: bool,
344 infos: &[CompileInfo],
345 warnings: &[CompileWarning],
346 errors: &[CompileError],
347 reverse_results: bool,
348) {
349 print_infos(source_engine, terse_mode, infos);
350
351 let e_len = errors.len();
352 let w_len = warnings.len();
353
354 if !terse_mode {
355 if reverse_results {
356 warnings
357 .iter()
358 .rev()
359 .for_each(|w| format_diagnostic(&w.to_diagnostic(source_engine)));
360 errors
361 .iter()
362 .rev()
363 .for_each(|e| format_diagnostic(&e.to_diagnostic(source_engine)));
364 } else {
365 warnings
366 .iter()
367 .for_each(|w| format_diagnostic(&w.to_diagnostic(source_engine)));
368 errors
369 .iter()
370 .for_each(|e| format_diagnostic(&e.to_diagnostic(source_engine)));
371 }
372 }
373
374 if e_len == 0 && w_len > 0 {
375 println_red_err(&format!(
376 " Aborting. {} warning(s) treated as error(s).",
377 warnings.len()
378 ));
379 } else {
380 println_red_err(&format!(
381 " Aborting due to {} {}.",
382 e_len,
383 if e_len > 1 { "errors" } else { "error" }
384 ));
385 }
386}
387
388pub fn create_diagnostics_renderer() -> Renderer {
393 Renderer::styled()
397 .warning(
398 Style::new()
399 .bold()
400 .fg_color(Some(AnsiColor::BrightYellow.into())),
401 )
402 .error(
403 Style::new()
404 .bold()
405 .fg_color(Some(AnsiColor::BrightRed.into())),
406 )
407}
408
409pub fn format_diagnostic(diagnostic: &Diagnostic) {
410 const SHOW_DIAGNOSTIC_CODE: bool = false;
413
414 if diagnostic.is_old_style() {
415 format_old_style_diagnostic(diagnostic.issue());
416 return;
417 }
418
419 let mut label = String::new();
420 get_title_label(diagnostic, &mut label);
421
422 let snippet_title = Some(Annotation {
423 label: Some(label.as_str()),
424 id: if SHOW_DIAGNOSTIC_CODE {
425 diagnostic.reason().map(|reason| reason.code())
426 } else {
427 None
428 },
429 annotation_type: diagnostic_level_to_annotation_type(diagnostic.level()),
430 });
431
432 let mut snippet_slices = Vec::<Slice<'_>>::new();
433
434 if diagnostic.issue().is_in_source() {
436 snippet_slices.push(construct_slice(diagnostic.labels_in_issue_source()))
437 }
438
439 for source_path in diagnostic.related_sources(false) {
441 snippet_slices.push(construct_slice(diagnostic.labels_in_source(source_path)))
442 }
443
444 let mut snippet_footer = Vec::<Annotation<'_>>::new();
445 for help in diagnostic.help() {
446 snippet_footer.push(Annotation {
447 id: None,
448 label: Some(help),
449 annotation_type: AnnotationType::Help,
450 });
451 }
452
453 let snippet = Snippet {
454 title: snippet_title,
455 slices: snippet_slices,
456 footer: snippet_footer,
457 };
458
459 let renderer = create_diagnostics_renderer();
460 match diagnostic.level() {
461 Level::Info => tracing::info!("{}\n____\n", renderer.render(snippet)),
462 Level::Warning => tracing::warn!("{}\n____\n", renderer.render(snippet)),
463 Level::Error => tracing::error!("{}\n____\n", renderer.render(snippet)),
464 }
465
466 fn format_old_style_diagnostic(issue: &Issue) {
467 let annotation_type = label_type_to_annotation_type(issue.label_type());
468
469 let snippet_title = Some(Annotation {
470 label: if issue.is_in_source() {
471 None
472 } else {
473 Some(issue.text())
474 },
475 id: None,
476 annotation_type,
477 });
478
479 let mut snippet_slices = vec![];
480 if issue.is_in_source() {
481 let span = issue.span();
482 let input = span.input();
483 let mut start_pos = span.start();
484 let mut end_pos = span.end();
485 let LineColRange { mut start, end } = span.line_col_one_index();
486 let input = construct_window(&mut start, end, &mut start_pos, &mut end_pos, input);
487
488 let slice = Slice {
489 source: input,
490 line_start: start.line,
491 origin: Some(issue.source_path().unwrap().as_str()),
493 fold: false,
494 annotations: vec![SourceAnnotation {
495 label: issue.text(),
496 annotation_type,
497 range: (start_pos, end_pos),
498 }],
499 };
500
501 snippet_slices.push(slice);
502 }
503
504 let snippet = Snippet {
505 title: snippet_title,
506 footer: vec![],
507 slices: snippet_slices,
508 };
509
510 let renderer = create_diagnostics_renderer();
511 tracing::error!("{}\n____\n", renderer.render(snippet));
512 }
513
514 fn get_title_label(diagnostics: &Diagnostic, label: &mut String) {
515 label.clear();
516 if let Some(reason) = diagnostics.reason() {
517 label.push_str(reason.description());
518 }
519 }
520
521 fn diagnostic_level_to_annotation_type(level: Level) -> AnnotationType {
522 match level {
523 Level::Info => AnnotationType::Info,
524 Level::Warning => AnnotationType::Warning,
525 Level::Error => AnnotationType::Error,
526 }
527 }
528}
529
530fn construct_slice(labels: Vec<&Label>) -> Slice {
531 debug_assert!(
532 !labels.is_empty(),
533 "To construct slices, at least one label must be provided."
534 );
535
536 debug_assert!(
537 labels.iter().all(|label| label.is_in_source()),
538 "Slices can be constructed only for labels that are related to a place in source code."
539 );
540
541 debug_assert!(
542 HashSet::<&str>::from_iter(labels.iter().map(|label| label.source_path().unwrap().as_str())).len() == 1,
543 "Slices can be constructed only for labels that are related to places in the same source code."
544 );
545
546 let source_file = labels[0].source_path().map(|path| path.as_str());
547 let source_code = labels[0].span().input();
548
549 let span = Span::join_all(labels.iter().map(|label| label.span().clone()));
551
552 let (source, line_start, shift_in_bytes) = construct_code_snippet(&span, source_code);
553
554 let mut annotations = vec![];
555
556 for message in labels {
557 annotations.push(SourceAnnotation {
558 label: message.text(),
559 annotation_type: label_type_to_annotation_type(message.label_type()),
560 range: get_annotation_range(message.span(), source_code, shift_in_bytes),
561 });
562 }
563
564 return Slice {
565 source,
566 line_start,
567 origin: source_file,
568 fold: true,
569 annotations,
570 };
571
572 fn get_annotation_range(
573 span: &Span,
574 source_code: &str,
575 shift_in_bytes: usize,
576 ) -> (usize, usize) {
577 let mut start_pos = span.start();
578 let mut end_pos = span.end();
579
580 let start_ix_bytes = start_pos - std::cmp::min(shift_in_bytes, start_pos);
581 let end_ix_bytes = end_pos - std::cmp::min(shift_in_bytes, end_pos);
582
583 start_pos = source_code[shift_in_bytes..(shift_in_bytes + start_ix_bytes)]
585 .chars()
586 .count();
587 end_pos = source_code[shift_in_bytes..(shift_in_bytes + end_ix_bytes)]
588 .chars()
589 .count();
590
591 (start_pos, end_pos)
592 }
593}
594
595fn label_type_to_annotation_type(label_type: LabelType) -> AnnotationType {
596 match label_type {
597 LabelType::Info => AnnotationType::Info,
598 LabelType::Help => AnnotationType::Help,
599 LabelType::Warning => AnnotationType::Warning,
600 LabelType::Error => AnnotationType::Error,
601 }
602}
603
604fn construct_code_snippet<'a>(span: &Span, input: &'a str) -> (&'a str, usize, usize) {
613 const NUM_LINES_BUFFER: usize = 2;
615
616 let LineColRange { start, end } = span.line_col_one_index();
617
618 let total_lines_in_input = input.chars().filter(|x| *x == '\n').count();
619 debug_assert!(end.line >= start.line);
620 let total_lines_of_highlight = end.line - start.line;
621 debug_assert!(total_lines_in_input >= total_lines_of_highlight);
622
623 let mut current_line = 0;
624 let mut lines_to_start_of_snippet = 0;
625 let mut calculated_start_ix = None;
626 let mut calculated_end_ix = None;
627 let mut pos = 0;
628 for character in input.chars() {
629 if character == '\n' {
630 current_line += 1
631 }
632
633 if current_line + NUM_LINES_BUFFER >= start.line && calculated_start_ix.is_none() {
634 calculated_start_ix = Some(pos);
635 lines_to_start_of_snippet = current_line;
636 }
637
638 if current_line >= end.line + NUM_LINES_BUFFER && calculated_end_ix.is_none() {
639 calculated_end_ix = Some(pos);
640 }
641
642 if calculated_start_ix.is_some() && calculated_end_ix.is_some() {
643 break;
644 }
645 pos += character.len_utf8();
646 }
647 let calculated_start_ix = calculated_start_ix.unwrap_or(0);
648 let calculated_end_ix = calculated_end_ix.unwrap_or(input.len());
649
650 (
651 &input[calculated_start_ix..calculated_end_ix],
652 lines_to_start_of_snippet,
653 calculated_start_ix,
654 )
655}
656
657fn construct_window<'a>(
666 start: &mut LineCol,
667 end: LineCol,
668 start_ix: &mut usize,
669 end_ix: &mut usize,
670 input: &'a str,
671) -> &'a str {
672 const NUM_LINES_BUFFER: usize = 2;
674
675 let total_lines_in_input = input.chars().filter(|x| *x == '\n').count();
676 debug_assert!(end.line >= start.line);
677 let total_lines_of_highlight = end.line - start.line;
678 debug_assert!(total_lines_in_input >= total_lines_of_highlight);
679
680 let mut current_line = 1usize;
681
682 let mut chars = input.char_indices().map(|(char_offset, character)| {
683 let r = (current_line, char_offset);
684 if character == '\n' {
685 current_line += 1;
686 }
687 r
688 });
689
690 let first_char = chars
692 .by_ref()
693 .find(|(current_line, _)| current_line + NUM_LINES_BUFFER >= start.line);
694
695 let last_char = chars
697 .by_ref()
698 .find(|(current_line, _)| *current_line > end.line + NUM_LINES_BUFFER)
699 .map(|x| x.1);
700
701 drop(chars);
703
704 let (first_char_line, first_char_offset, last_char_offset) = match (first_char, last_char) {
705 (Some((first_char_line, first_char_offset)), Some(last_char_offset)) => {
707 (first_char_line, first_char_offset, last_char_offset)
708 }
709 (Some((first_char_line, first_char_offset)), None) => {
711 (first_char_line, first_char_offset, input.len())
712 }
713 _ => (current_line, input.len(), input.len()),
715 };
716
717 start.line = first_char_line;
719 *start_ix = start_ix.saturating_sub(first_char_offset);
720 *end_ix = end_ix.saturating_sub(first_char_offset);
721
722 &input[first_char_offset..last_char_offset]
723}
724
725#[test]
726fn ok_construct_window() {
727 fn t(
728 start_line: usize,
729 start_col: usize,
730 end_line: usize,
731 end_col: usize,
732 start_char: usize,
733 end_char: usize,
734 input: &str,
735 ) -> (usize, usize, &str) {
736 let mut s = LineCol {
737 line: start_line,
738 col: start_col,
739 };
740 let mut start = start_char;
741 let mut end = end_char;
742 let r = construct_window(
743 &mut s,
744 LineCol {
745 line: end_line,
746 col: end_col,
747 },
748 &mut start,
749 &mut end,
750 input,
751 );
752 (start, end, r)
753 }
754
755 assert_eq!(t(0, 0, 0, 0, 0, 0, ""), (0, 0, ""));
757
758 assert_eq!(t(1, 1, 1, 1, 0, 0, ""), (0, 0, ""));
760
761 assert_eq!(t(1, 7, 1, 7, 6, 6, "script"), (6, 6, "script"));
763
764 let eight_lines = "1\n2\n3\n4\n5\n6\n7\n8";
766
767 assert_eq!(t(1, 1, 1, 1, 0, 1, eight_lines), (0, 1, "1\n2\n3\n"));
768 assert_eq!(t(2, 1, 2, 1, 2, 3, eight_lines), (2, 3, "1\n2\n3\n4\n"));
769 assert_eq!(t(3, 1, 3, 1, 4, 5, eight_lines), (4, 5, "1\n2\n3\n4\n5\n"));
770 assert_eq!(t(4, 1, 4, 1, 6, 7, eight_lines), (4, 5, "2\n3\n4\n5\n6\n"));
771 assert_eq!(t(5, 1, 5, 1, 8, 9, eight_lines), (4, 5, "3\n4\n5\n6\n7\n"));
772 assert_eq!(t(6, 1, 6, 1, 10, 11, eight_lines), (4, 5, "4\n5\n6\n7\n8"));
773 assert_eq!(t(7, 1, 7, 1, 12, 13, eight_lines), (4, 5, "5\n6\n7\n8"));
774 assert_eq!(t(8, 1, 8, 1, 14, 15, eight_lines), (4, 5, "6\n7\n8"));
775
776 assert_eq!(t(9, 1, 9, 1, 14, 15, eight_lines), (2, 3, "7\n8"));
778 assert_eq!(t(10, 1, 10, 1, 14, 15, eight_lines), (0, 1, "8"));
779 assert_eq!(t(11, 1, 11, 1, 14, 15, eight_lines), (0, 0, ""));
780}