Skip to main content

morph_cli/core/ast/
parser.rs

1use 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}