morph_cli/core/ast/
parser.rs1use std::error::Error;
2use std::fmt;
3use std::path::{Path, PathBuf};
4
5use swc_common::comments::SingleThreadedComments;
6use swc_common::sync::Lrc;
7use swc_common::{FileName, SourceMap};
8use swc_ecma_ast::Module;
9use swc_ecma_parser::lexer::Lexer;
10use swc_ecma_parser::{EsSyntax, Parser, StringInput, Syntax, TsSyntax};
11
12pub struct ParsedModule {
13 pub path: PathBuf,
14 pub module: Module,
15 pub source_map: Lrc<SourceMap>,
16 pub comments: SingleThreadedComments,
17}
18
19#[derive(Debug, Clone)]
20pub struct ParseFileError {
21 path: PathBuf,
22 details: String,
23}
24
25impl ParseFileError {
26 pub(crate) fn new(path: &Path, details: impl Into<String>) -> Self {
27 Self {
28 path: path.to_path_buf(),
29 details: details.into(),
30 }
31 }
32
33 pub fn path(&self) -> &Path {
34 &self.path
35 }
36}
37
38impl fmt::Display for ParseFileError {
39 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
40 write!(formatter, "{}: {}", self.path.display(), self.details)
41 }
42}
43
44impl Error for ParseFileError {}
45
46pub fn parse_file(path: &Path) -> Result<ParsedModule, ParseFileError> {
47 let source = std::fs::read_to_string(path)
48 .map_err(|error| ParseFileError::new(path, format!("failed to read file: {error}")))?;
49
50 parse_source(path, &source)
51}
52
53pub(crate) fn parse_source(path: &Path, source: &str) -> Result<ParsedModule, ParseFileError> {
54 let syntax = syntax_for_path(path)?;
55 let source_map: Lrc<SourceMap> = Lrc::new(SourceMap::default());
56 let comments = SingleThreadedComments::default();
57 let source_file =
58 source_map.new_source_file(FileName::Real(path.to_path_buf()).into(), source.to_owned());
59
60 let lexer = Lexer::new(
61 syntax,
62 Default::default(),
63 StringInput::from(&*source_file),
64 Some(&comments),
65 );
66 let mut parser = Parser::new_from(lexer);
67 let mut diagnostics = Vec::new();
68
69 for error in parser.take_errors() {
70 diagnostics.push(error.kind().msg().to_string());
71 }
72
73 let module = parser
74 .parse_module()
75 .map_err(|error| ParseFileError::new(path, error.kind().msg().to_string()))?;
76
77 for error in parser.take_errors() {
78 diagnostics.push(error.kind().msg().to_string());
79 }
80
81 if !diagnostics.is_empty() {
82 return Err(ParseFileError::new(path, diagnostics.join("; ")));
83 }
84
85 Ok(ParsedModule {
86 path: path.to_path_buf(),
87 module,
88 source_map,
89 comments,
90 })
91}
92
93fn syntax_for_path(path: &Path) -> Result<Syntax, ParseFileError> {
94 let extension = path
95 .extension()
96 .and_then(|extension| extension.to_str())
97 .ok_or_else(|| ParseFileError::new(path, "missing file extension"))?;
98
99 let syntax = match extension {
100 "js" | "mjs" | "cjs" | "jsx" => Syntax::Es(EsSyntax {
101 jsx: true,
102 ..Default::default()
103 }),
104 "ts" => Syntax::Typescript(TsSyntax {
105 tsx: false,
106 decorators: true,
107 ..Default::default()
108 }),
109 "tsx" => Syntax::Typescript(TsSyntax {
110 tsx: true,
111 decorators: true,
112 ..Default::default()
113 }),
114 _ => {
115 return Err(ParseFileError::new(
116 path,
117 format!("unsupported source extension `{extension}`"),
118 ));
119 }
120 };
121
122 Ok(syntax)
123}