summaryrefslogtreecommitdiff
path: root/cli/fmt_errors.rs
blob: 8432d36c2ec4f125845f8e020aeddd87bc81e068 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
//! This mod provides DenoError to unify errors across Deno.
use crate::colors;
use crate::source_maps::apply_source_map;
use crate::source_maps::SourceMapGetter;
use deno_core::error::AnyError;
use deno_core::error::JsError as CoreJsError;
use std::error::Error;
use std::fmt;
use std::ops::Deref;

const SOURCE_ABBREV_THRESHOLD: usize = 150;

pub fn format_location(filename: &str, line: i64, col: i64) -> String {
  format!(
    "{}:{}:{}",
    colors::cyan(filename),
    colors::yellow(&line.to_string()),
    colors::yellow(&col.to_string())
  )
}

pub fn format_stack(
  is_error: bool,
  message_line: &str,
  source_line: Option<&str>,
  start_column: Option<i64>,
  end_column: Option<i64>,
  formatted_frames: &[String],
  level: usize,
) -> String {
  let mut s = String::new();
  s.push_str(&format!("{:indent$}{}", "", message_line, indent = level));
  s.push_str(&format_maybe_source_line(
    source_line,
    start_column,
    end_column,
    is_error,
    level,
  ));
  for formatted_frame in formatted_frames {
    s.push_str(&format!(
      "\n{:indent$}    at {}",
      "",
      formatted_frame,
      indent = level
    ));
  }
  s
}

/// Take an optional source line and associated information to format it into
/// a pretty printed version of that line.
fn format_maybe_source_line(
  source_line: Option<&str>,
  start_column: Option<i64>,
  end_column: Option<i64>,
  is_error: bool,
  level: usize,
) -> String {
  if source_line.is_none() || start_column.is_none() || end_column.is_none() {
    return "".to_string();
  }

  let source_line = source_line.unwrap();
  // sometimes source_line gets set with an empty string, which then outputs
  // an empty source line when displayed, so need just short circuit here.
  // Also short-circuit on error line too long.
  if source_line.is_empty() || source_line.len() > SOURCE_ABBREV_THRESHOLD {
    return "".to_string();
  }

  assert!(start_column.is_some());
  assert!(end_column.is_some());
  let mut s = String::new();
  let start_column = start_column.unwrap();
  let end_column = end_column.unwrap();
  // TypeScript uses `~` always, but V8 would utilise `^` always, even when
  // doing ranges, so here, if we only have one marker (very common with V8
  // errors) we will use `^` instead.
  let underline_char = if (end_column - start_column) <= 1 {
    '^'
  } else {
    '~'
  };
  for _i in 0..start_column {
    if source_line.chars().nth(_i as usize).unwrap() == '\t' {
      s.push('\t');
    } else {
      s.push(' ');
    }
  }
  for _i in 0..(end_column - start_column) {
    s.push(underline_char);
  }
  let color_underline = if is_error {
    colors::red(&s).to_string()
  } else {
    colors::cyan(&s).to_string()
  };

  let indent = format!("{:indent$}", "", indent = level);

  format!("\n{}{}\n{}{}", indent, source_line, indent, color_underline)
}

/// Wrapper around deno_core::JsError which provides color to_string.
#[derive(Debug)]
pub struct JsError(CoreJsError);

impl JsError {
  pub fn create(
    core_js_error: CoreJsError,
    source_map_getter: &impl SourceMapGetter,
  ) -> AnyError {
    let core_js_error = apply_source_map(&core_js_error, source_map_getter);
    let js_error = Self(core_js_error);
    js_error.into()
  }
}

impl Deref for JsError {
  type Target = CoreJsError;
  fn deref(&self) -> &Self::Target {
    &self.0
  }
}

impl fmt::Display for JsError {
  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    let mut formatted_frames = self.0.formatted_frames.clone();

    // The formatted_frames passed from prepareStackTrace() are colored.
    if !colors::use_color() {
      formatted_frames = formatted_frames
        .iter()
        .map(|s| colors::strip_ansi_codes(s).to_string())
        .collect();
    }

    // When the stack frame array is empty, but the source location given by
    // (script_resource_name, line_number, start_column + 1) exists, this is
    // likely a syntax error. For the sake of formatting we treat it like it was
    // given as a single stack frame.
    if formatted_frames.is_empty()
      && self.0.script_resource_name.is_some()
      && self.0.line_number.is_some()
      && self.0.start_column.is_some()
    {
      formatted_frames = vec![format_location(
        self.0.script_resource_name.as_ref().unwrap(),
        self.0.line_number.unwrap(),
        self.0.start_column.unwrap() + 1,
      )]
    };

    write!(
      f,
      "{}",
      &format_stack(
        true,
        &self.0.message,
        self.0.source_line.as_deref(),
        self.0.start_column,
        self.0.end_column,
        &formatted_frames,
        0
      )
    )?;
    Ok(())
  }
}

impl Error for JsError {}

#[cfg(test)]
mod tests {
  use super::*;
  use crate::colors::strip_ansi_codes;

  #[test]
  fn test_format_none_source_line() {
    let actual = format_maybe_source_line(None, None, None, false, 0);
    assert_eq!(actual, "");
  }

  #[test]
  fn test_format_some_source_line() {
    let actual = format_maybe_source_line(
      Some("console.log('foo');"),
      Some(8),
      Some(11),
      true,
      0,
    );
    assert_eq!(
      strip_ansi_codes(&actual),
      "\nconsole.log(\'foo\');\n        ~~~"
    );
  }
}