use anyhow::anyhow;
use anyhow::bail;
use anyhow::Result;
use deno_ast::swc::parser::error::SyntaxError;
use deno_ast::swc::parser::Syntax;
use deno_ast::ModuleSpecifier;
use deno_ast::ParsedSource;
use std::path::Path;
use std::sync::Arc;
pub fn parse_swc_ast(file_path: &Path, file_extension: Option<&str>, file_text: Arc<str>) -> Result<ParsedSource> {
match parse_inner(file_path, file_extension, file_text.clone()) {
Ok(result) => Ok(result),
Err(err) => {
let lowercase_ext = file_extension.map(|ext| ext.to_string()).or_else(|| 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, None, file_text) {
Ok(result) => Ok(result),
Err(_) => Err(err), }
}
}
}
fn parse_inner(file_path: &Path, file_extension: Option<&str>, text: Arc<str>) -> Result<ParsedSource> {
let parsed_source = parse_inner_no_diagnostic_check(file_path, file_extension, text)?;
ensure_no_specific_syntax_errors(&parsed_source)?;
Ok(parsed_source)
}
fn parse_inner_no_diagnostic_check(file_path: &Path, file_extension: Option<&str>, text: Arc<str>) -> Result<ParsedSource> {
let media_type = if let Some(file_extension) = file_extension {
deno_ast::MediaType::from_path(&file_path.with_extension(file_extension))
} else {
deno_ast::MediaType::from_path(file_path)
};
let mut syntax = deno_ast::get_syntax(media_type);
if let Syntax::Es(es) = &mut syntax {
es.decorators = true;
}
deno_ast::parse_program(deno_ast::ParseParams {
specifier: path_to_specifier(file_path)?,
capture_tokens: true,
maybe_syntax: Some(syntax),
media_type,
scope_analysis: false,
text,
})
.map_err(|diagnostic| anyhow!("{:#}", &diagnostic))
}
fn path_to_specifier(path: &Path) -> Result<ModuleSpecifier> {
if let Some(specifier) = from_file_path(path) {
Ok(specifier)
} else if let Some(file_name) = path.file_name() {
match ModuleSpecifier::parse(&format!("file:///{}", file_name.to_string_lossy())) {
Ok(specifier) => Ok(specifier),
Err(err) => bail!("could not convert path to specifier: '{}', error: {:#}", path.display(), err),
}
} else {
bail!("could not convert path to specifier: '{}'", path.display())
}
}
fn from_file_path(path: &Path) -> Option<ModuleSpecifier> {
#[cfg(target_arch = "wasm32")]
{
from_file_path_wasm(path)
}
#[cfg(not(target_arch = "wasm32"))]
{
ModuleSpecifier::from_file_path(path).ok()
}
}
#[allow(unused)]
#[cfg(any(target_arch = "wasm32", test))]
fn from_file_path_wasm(path: &Path) -> Option<ModuleSpecifier> {
let mut parts = Vec::new();
for component in path.components() {
match component {
std::path::Component::Prefix(prefix) => {
let prefix = prefix.as_os_str().to_string_lossy();
parts.push(percent_encoding::utf8_percent_encode(prefix.as_ref(), percent_encoding::CONTROLS).to_string());
}
std::path::Component::RootDir => {
}
std::path::Component::CurDir | std::path::Component::ParentDir => {
return None;
}
std::path::Component::Normal(part) => {
parts.push(percent_encoding::percent_encode(part.as_encoded_bytes(), percent_encoding::CONTROLS).to_string());
}
}
}
ModuleSpecifier::parse(&format!("file:///{}", parts.join("/"))).ok()
}
pub fn ensure_no_specific_syntax_errors(parsed_source: &ParsedSource) -> Result<()> {
let diagnostics = parsed_source
.diagnostics()
.iter()
.filter(|e| {
matches!(
e.kind(),
SyntaxError::Eof |
SyntaxError::TS1003 |
SyntaxError::ExpectedIdent |
SyntaxError::TS1005 |
SyntaxError::ExpectedSemi |
SyntaxError::TS1109 |
SyntaxError::Expected(_, _) |
SyntaxError::ExpectedDigit { .. } |
SyntaxError::ExpectedSemiForExprStmt { .. } |
SyntaxError::ExpectedUnicodeEscape |
SyntaxError::UnterminatedStrLit |
SyntaxError::UnterminatedBlockComment |
SyntaxError::UnterminatedJSXContents |
SyntaxError::UnterminatedRegExp |
SyntaxError::UnterminatedTpl |
SyntaxError::Unexpected { .. } |
SyntaxError::TS1185
)
})
.collect::<Vec<_>>();
if diagnostics.is_empty() {
Ok(())
} else {
let mut final_message = String::new();
for diagnostic in diagnostics {
if !final_message.is_empty() {
final_message.push_str("\n\n");
}
final_message.push_str(&format!("{diagnostic}"));
}
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())
}
#[cfg(test)]
mod tests {
use crate::configuration::ConfigurationBuilder;
use pretty_assertions::assert_eq;
use super::*;
use std::path::PathBuf;
#[test]
fn test_from_file_path_wasm() {
fn run_test(path: &str, expected: Option<&str>) {
let actual = from_file_path_wasm(&PathBuf::from(path));
assert_eq!(actual.as_ref().map(|d| d.as_str()), expected);
}
run_test("C:\\Users\\user\\file.ts", Some("file:///C:/Users/user/file.ts"));
run_test("/file/other.ts", Some("file:///file/other.ts"));
}
#[test]
fn should_error_on_syntax_diagnostic() {
run_fatal_diagnostic_test(
"./test.ts",
"test;\nas#;",
concat!("Expected ';', '}' or <eof> at file:///test.ts:2:3\n", "\n", " as#;\n", " ~"),
);
}
#[test]
fn should_error_on_unary_expression_dot() {
run_fatal_diagnostic_test(
"./test.ts",
"+value.",
concat!(
"Expected ident at file:///test.ts:1:2\n\n",
" +value.\n",
" ~~~~~"
),
);
}
#[test]
fn should_error_on_unary_expression_dot_semicolon() {
run_fatal_diagnostic_test(
"./test.ts",
"+value.;",
concat!("Expected ident at file:///test.ts:1:8\n\n", " +value.;\n", " ~"),
);
}
#[test]
fn it_should_error_without_issue_when_there_exists_multi_byte_char_on_line_with_syntax_error() {
run_fatal_diagnostic_test(
"./test.ts",
concat!(
"test;\n",
r#"console.log('x', `duration ${d} not in range - ${min} ≥ ${d} && ${max} ≥ ${d}`),;"#,
),
concat!(
"Expression expected at file:///test.ts:2:81\n",
"\n",
" console.log('x', `duration ${d} not in range - ${min} ≥ ${d} && ${max} ≥ ${d}`),;\n",
" ~",
),
);
}
#[test]
fn it_should_error_closing_paren_missing() {
run_fatal_diagnostic_test(
"./test.ts",
r#"const foo = <T extends {}>() => {
if (bar() {
console.log(1);
}
};"#,
concat!(
"An arrow function is not allowed here at file:///test.ts:1:27\n",
"\n",
" const foo = <T extends {}>() => {\n",
" ~~"
),
);
}
#[test]
fn it_should_error_when_var_stmts_sep_by_comma() {
run_fatal_diagnostic_test(
"./test.ts",
"let a = 0, let b = 1;",
concat!(
"Unexpected token `let`. Expected let is reserved in const, let, class declaration at file:///test.ts:1:12\n",
"\n",
" let a = 0, let b = 1;\n",
" ~~~"
),
);
}
#[track_caller]
fn run_fatal_diagnostic_test(file_path: &str, text: &str, expected: &str) {
let file_path = PathBuf::from(file_path);
assert_eq!(parse_swc_ast(&file_path, None, text.into()).err().unwrap().to_string(), expected);
}
#[test]
fn it_should_error_for_no_equals_sign_in_var_decl() {
run_non_fatal_diagnostic_test(
"./test.ts",
"const Methods {\nf: (x, y) => x + y,\n};",
concat!(
"Expected a semicolon at file:///test.ts:1:15\n",
"\n",
" const Methods {\n",
" ~"
),
);
}
#[test]
fn it_should_error_for_exected_expr_issue_121() {
run_non_fatal_diagnostic_test(
"./test.ts",
"type T =\n | unknown\n { } & unknown;",
concat!("Expression expected at file:///test.ts:3:7\n\n", " { } & unknown;\n", " ~"),
);
}
#[test]
fn file_extension_overwrite() {
let file_path = PathBuf::from("./test.js");
assert!(parse_swc_ast(&file_path, Some("ts"), "const foo: string = 'bar';".into()).is_ok());
}
#[test]
fn it_should_error_for_exected_close_brace() {
run_fatal_diagnostic_test(
"./test.ts",
"class Test {",
concat!("Unexpected eof at file:///test.ts:1:13\n\n", " class Test {\n", " ~"),
);
}
#[test]
fn it_should_error_for_exected_string_literal() {
run_non_fatal_diagnostic_test(
"./test.ts",
"var foo = 'test",
concat!(
"Unterminated string constant at file:///test.ts:1:11\n\n",
" var foo = 'test\n",
" ~~~~~"
),
);
}
#[test]
fn it_should_error_for_merge_conflict_marker() {
run_non_fatal_diagnostic_test(
"./test.ts",
r#"class Test {
<<<<<<< HEAD
v = 1;
=======
v = 2;
>>>>>>> Branch-a
}
"#,
r#"Merge conflict marker encountered. at file:///test.ts:2:1
<<<<<<< HEAD
~~~~~~~
Merge conflict marker encountered. at file:///test.ts:4:1
=======
~~~~~~~
Merge conflict marker encountered. at file:///test.ts:6:1
>>>>>>> Branch-a
~~~~~~~"#,
);
}
#[track_caller]
fn run_non_fatal_diagnostic_test(file_path: &str, text: &str, expected: &str) {
let file_path = PathBuf::from(file_path);
assert_eq!(format!("{}", parse_swc_ast(&file_path, None, text.into()).err().unwrap()), expected);
let parsed_source = parse_inner_no_diagnostic_check(&file_path, None, text.into()).unwrap();
let config = ConfigurationBuilder::new().build();
assert_eq!(crate::format_parsed_source(&parsed_source, &config, None).err().unwrap().to_string(), expected);
}
}