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
use anyhow::anyhow;
use anyhow::bail;
use anyhow::Result;
use deno_ast::swc::parser::error::SyntaxError;
use deno_ast::Diagnostic;
use deno_ast::ParsedSource;
use deno_ast::SourceTextInfo;
use std::path::Path;
pub fn parse_swc_ast(file_path: &Path, file_text: &str) -> Result<ParsedSource> {
let file_text = SourceTextInfo::from_string(file_text.to_string());
match parse_inner(file_path, file_text.clone()) {
Ok(result) => Ok(result),
Err(err) => {
let lowercase_ext = get_lowercase_extension(file_path);
let new_file_path = match lowercase_ext.as_deref() {
Some("ts") | Some("cts") | Some("mts") => file_path.with_extension("tsx"),
Some("js") | Some("cjs") | Some("mjs") => file_path.with_extension("jsx"),
_ => return Err(err),
};
match parse_inner(&new_file_path, file_text) {
Ok(result) => Ok(result),
Err(_) => Err(err),
}
}
}
}
fn parse_inner(file_path: &Path, file_text: SourceTextInfo) -> Result<ParsedSource> {
let parsed_source = deno_ast::parse_program(deno_ast::ParseParams {
specifier: file_path.to_string_lossy().to_string(),
capture_tokens: true,
maybe_syntax: None,
media_type: file_path.into(),
scope_analysis: false,
source: file_text.clone(),
})
.map_err(|diagnostic| anyhow!("{}", format_diagnostic(&diagnostic, file_text.text_str())))?;
Ok(parsed_source)
}
pub fn ensure_no_specific_syntax_errors(parsed_source: &ParsedSource) -> Result<()> {
let diagnostics = parsed_source
.diagnostics()
.iter()
.filter(|e| {
matches!(
e.kind,
SyntaxError::TS1003 |
SyntaxError::TS1005 |
SyntaxError::TS1109 |
SyntaxError::Expected(_, _) |
SyntaxError::Unexpected { .. }
)
})
.collect::<Vec<_>>();
if diagnostics.is_empty() {
Ok(())
} else {
let mut final_message = String::new();
for error in diagnostics {
if !final_message.is_empty() {
final_message.push_str("\n\n");
}
final_message.push_str(&format_diagnostic(error, parsed_source.source().text_str()));
}
bail!("{}", final_message)
}
}
fn get_lowercase_extension(file_path: &Path) -> Option<String> {
file_path.extension().and_then(|e| e.to_str()).map(|f| f.to_lowercase())
}
fn format_diagnostic(error: &Diagnostic, file_text: &str) -> String {
let error_span = error.span;
dprint_core::formatting::utils::string_utils::format_diagnostic(Some((error_span.lo().0 as usize, error_span.hi().0 as usize)), &error.message(), file_text)
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn should_error_on_syntax_diagnostic() {
let message = parse_swc_ast(&PathBuf::from("./test.ts"), "test;\nas#;").err().unwrap().to_string();
assert_eq!(message, concat!("Line 2, column 3: Expected ';', '}' or <eof>\n", "\n", " as#;\n", " ~"));
}
#[test]
fn it_should_error_without_issue_when_there_exists_multi_byte_char_on_line_with_syntax_error() {
let message = parse_swc_ast(
&PathBuf::from("./test.ts"),
concat!(
"test;\n",
r#"console.log("x", `duration ${d} not in range - ${min} ≥ ${d} && ${max} ≥ ${d}`),;"#,
),
)
.err()
.unwrap()
.to_string();
assert_eq!(
message,
concat!(
"Line 2, column 81: Unexpected token `;`. Expected this, import, async, function, [ for array literal, ",
"{ for object literal, @ for decorator, function, class, null, true, false, number, bigint, string, ",
"regexp, ` for template literal, (, or an identifier\n",
"\n",
" && ${max} ≥ ${d}`),;\n",
" ~"
)
);
}
}