Skip to main content

oxc_parser/
observation.rs

1//! Immutable authority from the original parser's complete observation.
2
3use oxc_ast::ast::{Comment, Program};
4use oxc_diagnostics::Diagnostics;
5use oxc_span::SourceType;
6
7use crate::{
8    ParseOptions, Parser, ParserReturn,
9    config::{NoTokensParserConfig, ParserConfig, RuntimeParserConfig, TokensParserConfig},
10};
11
12mod default_export;
13pub use default_export::OriginalDefaultExport;
14
15/// The complete ordinarily owned result of one parser invocation.
16///
17/// Private storage prevents a caller from discarding diagnostics and presenting
18/// a recovered AST as a clean parse. This is syntax admission, not semantic
19/// validation. There is no mutable AST access or unchecked extraction.
20/// Shared structure does not freeze the AST's semantic ID Cells.
21///
22/// Raw public parser returns cannot establish this authority:
23///
24/// ```compile_fail
25/// use oxc_parser::{ParserReturn, ProgramObservation};
26/// fn forge<'a>(parsed: ParserReturn<'a>) -> ProgramObservation<'a> {
27///     ProgramObservation { parsed, source_text: "", source_type: Default::default(), options: Default::default() }
28/// }
29/// ```
30pub struct ProgramObservation<'a> {
31    parsed: ParserReturn<'a>,
32    source_text: &'a str,
33    source_type: SourceType,
34    options: ParseOptions,
35}
36
37/// A short immutable borrow minted only by a clean original observation.
38///
39/// The owner borrow cannot be replaced with an arbitrary Program:
40///
41/// ```compile_fail
42/// use oxc_parser::{AdmittedProgram, ProgramObservation};
43/// fn forge<'p, 'a>(observation: &'p ProgramObservation<'a>) -> AdmittedProgram<'p, 'a> {
44///     AdmittedProgram { observation }
45/// }
46/// ```
47///
48/// A live capability exposes only shared syntax:
49///
50/// ```compile_fail
51/// use oxc_ast::ast::Program;
52/// use oxc_parser::AdmittedProgram;
53/// fn mutate<'p, 'a>(admitted: &'p AdmittedProgram<'p, 'a>) -> &'p mut Program<'a> {
54///     admitted.program()
55/// }
56/// ```
57pub struct AdmittedProgram<'p, 'a> {
58    observation: &'p ProgramObservation<'a>,
59}
60
61impl<'a> Parser<'a, NoTokensParserConfig> {
62    /// Retain the actual source/profile/options and complete result of one parse.
63    ///
64    /// Only the parser's built-in configurations can establish this authority.
65    /// A custom configuration still supports ordinary `parse`, but cannot
66    /// substitute lexer handlers and mint native compiler admission:
67    ///
68    /// ```compile_fail,E0599
69    /// use oxc_allocator::Allocator;
70    /// use oxc_parser::{Parser, config::{NoTokensLexerConfig, ParserConfig}};
71    /// use oxc_span::SourceType;
72    /// #[derive(Default)]
73    /// struct Custom;
74    /// impl ParserConfig for Custom {
75    ///     type LexerConfig = NoTokensLexerConfig;
76    ///     fn lexer_config(&self) -> NoTokensLexerConfig { NoTokensLexerConfig }
77    /// }
78    /// let allocator = Allocator::default();
79    /// Parser::new(&allocator, "const value = 1;", SourceType::mjs())
80    ///     .with_config(Custom).parse_observed();
81    /// ```
82    pub fn parse_observed(self) -> ProgramObservation<'a> {
83        observe_parser(self)
84    }
85}
86
87impl<'a> Parser<'a, TokensParserConfig> {
88    /// Retain one complete observation using the built-in token configuration.
89    pub fn parse_observed(self) -> ProgramObservation<'a> {
90        observe_parser(self)
91    }
92}
93
94impl<'a> Parser<'a, RuntimeParserConfig> {
95    /// Retain one complete observation using the built-in runtime configuration.
96    pub fn parse_observed(self) -> ProgramObservation<'a> {
97        observe_parser(self)
98    }
99}
100
101// Private sharing cannot grant an unknown ParserConfig a public authority path.
102fn observe_parser<'a, C: ParserConfig>(parser: Parser<'a, C>) -> ProgramObservation<'a> {
103    let source_text = parser.source_text;
104    let source_type = parser.source_type;
105    let options = parser.options;
106    let parsed = parser.parse();
107    ProgramObservation { parsed, source_text, source_type, options }
108}
109
110impl<'a> ProgramObservation<'a> {
111    /// Recovered, fatal and Flow results never mint this capability.
112    #[must_use]
113    pub fn admitted(&self) -> Option<AdmittedProgram<'_, 'a>> {
114        (!self.parsed.panicked
115            && !self.parsed.is_flow_language
116            && !self.parsed.diagnostics.has_errors())
117        .then_some(AdmittedProgram { observation: self })
118    }
119
120    #[must_use]
121    pub fn diagnostics(&self) -> &Diagnostics {
122        &self.parsed.diagnostics
123    }
124
125    #[must_use]
126    pub fn comments(&self) -> &[Comment] {
127        self.parsed.program.comments.as_slice()
128    }
129
130    #[must_use]
131    pub fn panicked(&self) -> bool {
132        self.parsed.panicked
133    }
134
135    #[must_use]
136    pub fn is_flow_language(&self) -> bool {
137        self.parsed.is_flow_language
138    }
139}
140
141impl<'p, 'a> AdmittedProgram<'p, 'a> {
142    #[must_use]
143    pub fn program(&self) -> &'p Program<'a> {
144        &self.observation.parsed.program
145    }
146
147    /// Actual original bytes, including comments and line endings.
148    #[must_use]
149    pub fn source(&self) -> &'a str {
150        self.observation.source_text
151    }
152
153    /// Whether the original lexer decoded a legacy numeric literal or string
154    /// escape forbidden in a strict module. This does not certify semantics.
155    /// Syntax diagnostics and ordinary parser output remain unchanged.
156    #[must_use]
157    pub fn has_legacy_literals(&self) -> bool {
158        self.observation.parsed.has_legacy_literals
159    }
160
161    /// Whether the original trivia classifier observed JSDoc or legal JSDoc.
162    /// This is presence only; it grants no effective type or binding semantics.
163    /// Checkpoints restore speculation, while committed reparses retain the fact.
164    #[must_use]
165    pub fn has_jsdoc_comments(&self) -> bool {
166        self.observation.parsed.has_jsdoc_comments
167    }
168
169    /// Original requested profile, before any Unambiguous inference.
170    #[must_use]
171    pub fn source_type(&self) -> SourceType {
172        self.observation.source_type
173    }
174
175    /// Actual parser options; admission does not imply default compiler options.
176    #[must_use]
177    pub fn options(&self) -> ParseOptions {
178        self.observation.options
179    }
180}
181
182#[cfg(test)]
183mod legacy_literal_tests {
184    use super::*;
185    use oxc_allocator::Allocator;
186
187    #[test]
188    fn completed_tail_receipt_survives_committed_unambiguous_await_reparse() {
189        let arena = Allocator::default();
190        for literal in ["010", "'\\8'"] {
191            let source = format!("await /x/u; export {{}}; const value={literal};");
192            let parsed = Parser::new(&arena, &source, SourceType::unambiguous()).parse_observed();
193            let admitted = parsed.admitted().expect("completed original syntax");
194            assert!(admitted.has_legacy_literals(), "{literal}");
195            assert!(admitted.program().source_type.is_module());
196        }
197    }
198
199    #[test]
200    fn original_decoder_retains_legacy_facts_without_changing_syntax_admission() {
201        let arena = Allocator::default();
202        for literal in ["010", "08", "09.5", "'\\1'", "'\\8'", "'\\9'", "'\\00'"] {
203            let source = format!("const value={literal};");
204            let parsed = Parser::new(&arena, &source, SourceType::mjs()).parse_observed();
205            let admitted = parsed.admitted().expect("unchanged syntax admission");
206            assert!(admitted.has_legacy_literals(), "{literal}");
207            assert!(parsed.diagnostics().is_empty());
208            assert_eq!(admitted.source(), source);
209        }
210        for literal in [
211            "0",
212            "0o10",
213            "0x10",
214            "0b10",
215            "0.1",
216            "0e1",
217            "'雪'",
218            "'\\0'",
219            "'\\x01'",
220            "'\\u0001'",
221            "'\\\\1'",
222            "'\\🌸'",
223        ] {
224            let source = format!("const value={literal};");
225            let parsed = Parser::new(&arena, &source, SourceType::mjs()).parse_observed();
226            assert!(
227                !parsed.admitted().expect("actual admission").has_legacy_literals(),
228                "{literal}"
229            );
230        }
231    }
232}
233
234#[cfg(test)]
235mod jsdoc_tests;