Skip to main content

flowjs_parser/
lib.rs

1//! Typed Rust bindings for Facebook's Flow parser via QuickJS.
2//!
3//! ```rust
4//! use flowjs_parser::FlowParser;
5//!
6//! let parser = FlowParser::new().unwrap();
7//!
8//! // Parse and get a typed AST
9//! let ast = parser.parse("type Foo = string;").unwrap();
10//!
11//! // Validate that source is syntactically valid Flow
12//! parser.validate("type Bar = {| +x: number |}").unwrap();
13//! ```
14
15mod ast;
16
17pub use ast::*;
18
19use quick_js::Context;
20
21const FLOW_PARSER_JS: &str = include_str!(concat!(env!("OUT_DIR"), "/flow_parser.js"));
22
23/// Flow parser error.
24#[derive(Debug)]
25pub enum Error {
26    /// QuickJS runtime error.
27    Runtime(String),
28    /// Flow parser reported syntax errors. Each entry carries message and location.
29    Parse(Vec<ParseError>),
30    /// Failed to deserialize the AST JSON.
31    Deserialize(String),
32}
33
34impl std::fmt::Display for Error {
35    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36        match self {
37            Self::Runtime(msg) => write!(f, "runtime error: {msg}"),
38            Self::Parse(errors) => {
39                let msgs: Vec<&str> = errors.iter().map(|e| e.message.as_str()).collect();
40                write!(f, "parse errors: {}", msgs.join("; "))
41            }
42            Self::Deserialize(msg) => write!(f, "AST deserialization error: {msg}"),
43        }
44    }
45}
46
47impl std::error::Error for Error {}
48
49/// A reusable Flow parser instance backed by QuickJS.
50///
51/// Create once, parse many times. The QuickJS context is reused across calls.
52pub struct FlowParser {
53    ctx: Context,
54}
55
56impl FlowParser {
57    /// Create a new parser instance.
58    ///
59    /// Loads the Flow parser JS into a QuickJS context.
60    pub fn new() -> Result<Self, Error> {
61        let ctx = Context::new().map_err(|e| Error::Runtime(e.to_string()))?;
62
63        ctx.eval(
64            "var exports = {}; \
65             var console = { log: function(){}, error: function(){}, warn: function(){} };",
66        )
67        .map_err(|e| Error::Runtime(e.to_string()))?;
68
69        ctx.eval(FLOW_PARSER_JS)
70            .map_err(|e| Error::Runtime(e.to_string()))?;
71
72        Ok(Self { ctx })
73    }
74
75    /// Parse Flow source and return the raw JSON AST.
76    pub fn parse_json(&self, source: &str) -> Result<String, Error> {
77        let source_json =
78            serde_json::to_string(source).map_err(|e| Error::Runtime(e.to_string()))?;
79
80        let script = format!(
81            "(function() {{\
82                var ast = exports.parse({source_json}, {{ types: true, enums: true }});\
83                return JSON.stringify(ast);\
84            }})()"
85        );
86
87        self.ctx
88            .eval_as(&script)
89            .map_err(|e| Error::Runtime(e.to_string()))
90    }
91
92    /// Parse Flow source and return the typed AST.
93    pub fn parse(&self, source: &str) -> Result<Program, Error> {
94        let json = self.parse_json(source)?;
95
96        let program: Program =
97            serde_json::from_str(&json).map_err(|e| Error::Deserialize(e.to_string()))?;
98
99        if !program.errors.is_empty() {
100            return Err(Error::Parse(program.errors));
101        }
102
103        Ok(program)
104    }
105
106    /// Validate that source is syntactically valid Flow.
107    ///
108    /// Returns `Ok(())` if the source parses without errors, `Err` otherwise.
109    pub fn validate(&self, source: &str) -> Result<(), Error> {
110        self.parse(source).map(|_| ())
111    }
112
113    /// Parse source and return all diagnostics (parse errors with location).
114    ///
115    /// Returns `Ok(vec![])` for valid source. Returns `Ok(errors)` when the
116    /// source has parse errors — each error carries a message and source location.
117    /// Returns `Err` only on runtime or deserialization failure.
118    pub fn diagnostics(&self, source: &str) -> Result<Vec<ParseError>, Error> {
119        let json = self.parse_json(source)?;
120        let program: Program =
121            serde_json::from_str(&json).map_err(|e| Error::Deserialize(e.to_string()))?;
122        Ok(program.errors)
123    }
124
125    /// Validate a Flow type declaration by wrapping it in a `// @flow` file.
126    ///
127    /// Accepts a bare declaration like `type Foo = string;` and wraps it as:
128    /// ```text
129    /// // @flow
130    /// export type Foo = string;
131    /// ```
132    ///
133    /// Declarations starting with `declare export` are wrapped without adding
134    /// `export` (they already include it).
135    pub fn validate_declaration(&self, decl: &str) -> Result<Program, Error> {
136        let source = if decl.starts_with("declare export") {
137            format!("// @flow\n{decl}\n")
138        } else {
139            format!("// @flow\nexport {decl}\n")
140        };
141        self.parse(&source)
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    #[test]
150    fn parse_type_alias() {
151        // Arrange
152        let parser = FlowParser::new().expect("failed to create parser");
153
154        // Act
155        let program = parser
156            .parse("export type Foo = string;")
157            .expect("failed to parse");
158
159        // Assert
160        assert_eq!(program.body.len(), 1, "should have one statement");
161        match &program.body[0] {
162            Statement::ExportNamedDeclaration {
163                declaration: Some(Declaration::TypeAlias { id, right }),
164            } => {
165                assert_eq!(id.name, "Foo", "type name");
166                assert_eq!(right.type_name(), "StringTypeAnnotation", "right-hand side");
167            }
168            other => panic!("unexpected statement: {other:?}"),
169        }
170    }
171
172    #[test]
173    fn parse_exact_object() {
174        // Arrange
175        let parser = FlowParser::new().expect("failed to create parser");
176
177        // Act
178        let program = parser
179            .parse("export type T = {| +name: string, +age: number |};")
180            .expect("failed to parse");
181
182        // Assert
183        match &program.body[0] {
184            Statement::ExportNamedDeclaration {
185                declaration: Some(Declaration::TypeAlias { right, .. }),
186            } => match right {
187                TypeAnnotation::ObjectTypeAnnotation {
188                    properties, exact, ..
189                } => {
190                    assert!(*exact, "should be exact object");
191                    assert_eq!(properties.len(), 2, "should have 2 properties");
192                }
193                other => panic!("expected ObjectTypeAnnotation, got: {}", other.type_name()),
194            },
195            other => panic!("unexpected statement: {other:?}"),
196        }
197    }
198
199    #[test]
200    fn parse_error_reported() {
201        // Arrange
202        let parser = FlowParser::new().expect("failed to create parser");
203
204        // Act
205        let result = parser.parse("type = ;");
206
207        // Assert
208        assert!(result.is_err(), "should report parse error");
209    }
210
211    #[test]
212    fn parser_reusable() {
213        // Arrange
214        let parser = FlowParser::new().expect("failed to create parser");
215
216        // Act and Assert
217        assert!(parser.parse("type A = string;").is_ok(), "first parse");
218        assert!(parser.parse("type B = number;").is_ok(), "second parse");
219    }
220
221    #[test]
222    fn validate_valid_source() {
223        // Arrange
224        let parser = FlowParser::new().expect("failed to create parser");
225
226        // Act and Assert
227        assert!(
228            parser.validate("type T = {| +x: number |}").is_ok(),
229            "valid Flow should pass"
230        );
231    }
232
233    #[test]
234    fn validate_invalid_source() {
235        // Arrange
236        let parser = FlowParser::new().expect("failed to create parser");
237
238        // Act and Assert
239        assert!(
240            parser.validate("type = ;").is_err(),
241            "invalid Flow should fail"
242        );
243    }
244
245    #[test]
246    fn validate_declaration_type_alias() {
247        // Arrange
248        let parser = FlowParser::new().expect("failed to create parser");
249
250        // Act
251        let program = parser
252            .validate_declaration("type Foo = string;")
253            .expect("should parse");
254
255        // Assert
256        match &program.body[0] {
257            Statement::ExportNamedDeclaration {
258                declaration: Some(Declaration::TypeAlias { id, .. }),
259            } => {
260                assert_eq!(id.name, "Foo", "type name preserved");
261            }
262            other => panic!("unexpected statement: {other:?}"),
263        }
264    }
265
266    #[test]
267    fn validate_declaration_opaque() {
268        // Arrange
269        let parser = FlowParser::new().expect("failed to create parser");
270
271        // Act
272        let program = parser
273            .validate_declaration("declare export opaque type Token;")
274            .expect("should parse");
275
276        // Assert
277        match &program.body[0] {
278            Statement::DeclareExportDeclaration {
279                declaration: Some(decl),
280            } => match decl {
281                Declaration::OpaqueType { id, .. } | Declaration::DeclareOpaqueType { id, .. } => {
282                    assert_eq!(id.name, "Token", "opaque type name");
283                }
284                other => panic!("expected OpaqueType, got: {other:?}"),
285            },
286            other => panic!("unexpected statement: {other:?}"),
287        }
288    }
289
290    #[test]
291    fn diagnostics_valid_source_returns_empty() {
292        // Arrange
293        let parser = FlowParser::new().expect("failed to create parser");
294
295        // Act
296        let diags = parser
297            .diagnostics("// @flow\nexport type Foo = string;")
298            .expect("runtime error");
299
300        // Assert
301        assert!(
302            diags.is_empty(),
303            "valid source should produce no diagnostics"
304        );
305    }
306
307    #[test]
308    fn diagnostics_error_without_loc_deserializes() {
309        // Arrange — synthesize a payload where loc is absent
310        let payload = r#"{"type":"Program","body":[],"errors":[{"message":"synthetic error"}]}"#;
311
312        // Act
313        let program: Program = serde_json::from_str(payload).expect("should deserialize");
314
315        // Assert
316        assert_eq!(program.errors.len(), 1, "should have one error");
317        assert_eq!(
318            program.errors[0].message, "synthetic error",
319            "message preserved"
320        );
321        assert!(
322            program.errors[0].loc.is_none(),
323            "loc should be None when absent"
324        );
325    }
326
327    #[test]
328    fn diagnostics_invalid_source_returns_errors_with_location() {
329        // Arrange
330        let parser = FlowParser::new().expect("failed to create parser");
331
332        // Act
333        let diags = parser.diagnostics("type = ;").expect("runtime error");
334
335        // Assert
336        assert!(
337            !diags.is_empty(),
338            "invalid source should produce diagnostics"
339        );
340        let first = &diags[0];
341        assert!(
342            !first.message.is_empty(),
343            "diagnostic should have a message"
344        );
345        let loc = first.loc.as_ref().expect("diagnostic should have location");
346        assert!(loc.start.line >= 1, "start line should be >= 1");
347    }
348}