1use super::{FileProducer, ProgramInput, ProgramInputError, ProgramScope};
4use crate::artifact::ArtifactError;
5use crate::file::{FileArtifact, RejectedFile};
6use alloc::boxed::Box;
7use oxc_parser::ProgramObservation;
8use vize_l0::{Allocator, SourceBlock};
9
10mod recorder;
11mod records;
12use recorder::Recorder;
13pub use records::{JsxChildren, JsxNode};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum JsxFileError {
17 SyntaxAdmission,
18 Input(ProgramInputError),
19 Artifact(ArtifactError),
20 InvalidProfile,
21 AlreadyWalked,
22 NotWalked,
23 Interrupted,
24 IncompleteFile,
25 IncompleteBody,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29enum Phase {
30 Pending,
31 Interrupted,
32 Complete,
33}
34
35pub struct JsxFileProducer<'a> {
47 observation: ProgramObservation<'a>,
48 block: SourceBlock<'a>,
49 index: usize,
50 producer: FileProducer<'a>,
51 recorder: Recorder<'a>,
52 phase: Phase,
53}
54
55pub struct JsxFile<'a> {
62 observation: ProgramObservation<'a>,
63 file: FileArtifact<'a>,
64 records: alloc::vec::Vec<records::Record<'a>>,
65}
66
67pub struct RejectedJsxFile<'a> {
69 observation: ProgramObservation<'a>,
70 error: JsxFileError,
71 file: Option<FileArtifact<'a>>,
72 rejected_file: Option<Box<RejectedFile<'a>>>,
73 records: alloc::vec::Vec<records::Record<'a>>,
74}
75
76impl<'a> JsxFileProducer<'a> {
77 pub fn new(
78 allocator: &'a Allocator,
79 observation: ProgramObservation<'a>,
80 block: SourceBlock<'a>,
81 container_index: usize,
82 ) -> Result<Self, Box<RejectedJsxFile<'a>>> {
83 let admission = observation
84 .admitted()
85 .ok_or(JsxFileError::SyntaxAdmission)
86 .and_then(|admitted| {
87 let input = ProgramInput::checked(admitted, block, container_index)
88 .map_err(JsxFileError::Input)?;
89 if !input.source_type().is_jsx() || !input.source_type().is_module() {
90 return Err(JsxFileError::InvalidProfile);
91 }
92 Ok(())
93 });
94 if let Err(error) = admission {
95 return Err(Box::new(RejectedJsxFile::new(observation, error)));
96 }
97 let producer = match FileProducer::new(allocator, block.root_source()) {
98 Ok(producer) => producer,
99 Err(error) => {
100 return Err(Box::new(RejectedJsxFile::new(
101 observation,
102 JsxFileError::Artifact(error),
103 )));
104 }
105 };
106 Ok(Self {
107 observation,
108 block,
109 index: container_index,
110 producer,
111 recorder: Recorder::new(),
112 phase: Phase::Pending,
113 })
114 }
115
116 pub fn walk(&mut self) -> Result<(), JsxFileError> {
119 self.walk_with(|producer, input, recorder| {
120 producer
121 .program_observed(input, ProgramScope::Module, recorder)
122 .map(|_| ())
123 })
124 }
125
126 fn walk_with(
127 &mut self,
128 visit: impl FnOnce(
129 &mut FileProducer<'a>,
130 ProgramInput<'_, 'a>,
131 &mut Recorder<'a>,
132 ) -> Result<(), ProgramInputError>,
133 ) -> Result<(), JsxFileError> {
134 if self.phase != Phase::Pending {
135 return Err(JsxFileError::AlreadyWalked);
136 }
137 self.phase = Phase::Interrupted;
138 let admitted = self
139 .observation
140 .admitted()
141 .ok_or(JsxFileError::SyntaxAdmission)?;
142 let input =
143 ProgramInput::checked(admitted, self.block, self.index).map_err(JsxFileError::Input)?;
144 visit(&mut self.producer, input, &mut self.recorder).map_err(JsxFileError::Input)?;
145 self.phase = Phase::Complete;
146 Ok(())
147 }
148
149 #[must_use]
150 pub fn observation(&self) -> &ProgramObservation<'a> {
151 &self.observation
152 }
153
154 pub fn finish(self) -> Result<JsxFile<'a>, Box<RejectedJsxFile<'a>>> {
155 let Self {
156 observation,
157 producer,
158 recorder,
159 phase,
160 ..
161 } = self;
162 let body_complete = recorder.frames_complete() && recorder.jsx_count() > 0;
163 let mut rejection = RejectedJsxFile::new(
164 observation,
165 match phase {
166 Phase::Pending => JsxFileError::NotWalked,
167 Phase::Interrupted => JsxFileError::Interrupted,
168 Phase::Complete => JsxFileError::IncompleteBody,
169 },
170 );
171 rejection.records = recorder.records;
172 let file = match producer.finish() {
173 Ok(file) => file,
174 Err(file) => {
175 rejection.error = JsxFileError::Artifact(file.artifact.error);
176 rejection.rejected_file = Some(Box::new(file));
177 return Err(Box::new(rejection));
178 }
179 };
180 if phase != Phase::Complete {
181 rejection.file = Some(file);
182 return Err(Box::new(rejection));
183 }
184 if !file.is_complete() {
186 rejection.error = JsxFileError::IncompleteFile;
187 rejection.file = Some(file);
188 return Err(Box::new(rejection));
189 }
190 if !body_complete {
191 rejection.file = Some(file);
192 return Err(Box::new(rejection));
193 }
194 Ok(JsxFile {
195 observation: rejection.observation,
196 file,
197 records: rejection.records,
198 })
199 }
200}
201
202impl<'a> JsxFile<'a> {
203 #[must_use]
204 pub fn observation(&self) -> &ProgramObservation<'a> {
205 &self.observation
206 }
207 #[must_use]
208 pub fn file(&self) -> &FileArtifact<'a> {
209 &self.file
210 }
211 #[must_use]
212 pub fn node(&self, index: usize) -> Option<JsxNode<'_, 'a>> {
213 self.records.get(index)?;
214 Some(JsxNode::new(self, index))
215 }
216 pub fn nodes(&self) -> impl Iterator<Item = JsxNode<'_, 'a>> {
217 (0..self.records.len()).map(|index| JsxNode::new(self, index))
218 }
219}
220
221impl<'a> RejectedJsxFile<'a> {
222 fn new(observation: ProgramObservation<'a>, error: JsxFileError) -> Self {
223 Self {
224 observation,
225 error,
226 file: None,
227 rejected_file: None,
228 records: alloc::vec::Vec::new(),
229 }
230 }
231 #[must_use]
232 pub fn error(&self) -> JsxFileError {
233 self.error
234 }
235 #[must_use]
236 pub fn observation(&self) -> &ProgramObservation<'a> {
237 &self.observation
238 }
239 #[must_use]
240 pub fn file(&self) -> Option<&FileArtifact<'a>> {
241 self.file.as_ref()
242 }
243 #[must_use]
244 pub fn rejected_file(&self) -> Option<&RejectedFile<'a>> {
245 self.rejected_file.as_deref()
246 }
247 #[must_use]
249 pub fn recorded_nodes(&self) -> usize {
250 self.records.len()
251 }
252}
253
254#[cfg(test)]
255mod tests;