1use std::{collections::BTreeMap, error::Error, fmt};
4
5use boxferry_engine::{NativeFinding, NativeFindingLabel, NativeFindingLabelKind, Severity};
6use boxferry_model::{Identifier, ModelError, SourceId};
7use quadlet_lens::{
8 diagnostic::{Diagnostic as NativeDiagnostic, Severity as NativeSeverity},
9 model::{
10 DocumentSetError, NamedQuadletDocument, QuadletDocument, QuadletDocumentSet, TypedModelError, UnitFileName,
11 },
12 source::{SourceId as QuadletSourceId, SourceSpan as QuadletSourceSpan},
13};
14
15#[derive(Clone, Copy, Debug, Eq, PartialEq)]
17#[non_exhaustive]
18pub enum QuadletParseDiagnosticOrigin {
19 Syntax,
21 Model,
23 DocumentSet,
25}
26
27#[derive(Clone, Copy, Debug, Eq, PartialEq)]
29#[non_exhaustive]
30pub enum QuadletParseDiagnosticSeverity {
31 Error,
33 Warning,
35 Note,
37}
38
39#[derive(Clone, Debug, Eq, PartialEq)]
44#[non_exhaustive]
45pub struct QuadletParseDiagnosticLabel {
46 source_id: u32,
47 start: usize,
48 end: usize,
49 message: &'static str,
50}
51
52impl QuadletParseDiagnosticLabel {
53 #[must_use]
55 pub const fn source_id(&self) -> u32 {
56 self.source_id
57 }
58
59 #[must_use]
61 pub const fn start(&self) -> usize {
62 self.start
63 }
64
65 #[must_use]
67 pub const fn end(&self) -> usize {
68 self.end
69 }
70
71 #[must_use]
73 pub const fn message(&self) -> &'static str {
74 self.message
75 }
76}
77
78#[derive(Clone, Debug, Eq, PartialEq)]
80#[non_exhaustive]
81pub struct QuadletParseDiagnostic {
82 code: &'static str,
83 severity: QuadletParseDiagnosticSeverity,
84 summary: &'static str,
85 origin: QuadletParseDiagnosticOrigin,
86 labels: Vec<QuadletParseDiagnosticLabel>,
87}
88
89impl QuadletParseDiagnostic {
90 #[must_use]
92 pub const fn code(&self) -> &'static str {
93 self.code
94 }
95
96 #[must_use]
98 pub const fn severity(&self) -> QuadletParseDiagnosticSeverity {
99 self.severity
100 }
101
102 #[must_use]
104 pub const fn summary(&self) -> &'static str {
105 self.summary
106 }
107
108 #[must_use]
110 pub const fn origin(&self) -> QuadletParseDiagnosticOrigin {
111 self.origin
112 }
113
114 #[must_use]
116 pub fn labels(&self) -> &[QuadletParseDiagnosticLabel] {
117 &self.labels
118 }
119
120 #[must_use]
122 pub fn native_finding(&self) -> NativeFinding {
123 native_finding(self)
124 }
125}
126
127#[derive(Clone, Copy, Debug, Eq, PartialEq)]
129#[non_exhaustive]
130pub enum QuadletParseFailureStage {
131 Filename,
133 TypedModel,
135 DocumentMetadata,
137 DocumentSet,
139 NeutralModel,
141}
142
143#[derive(Clone, Debug, Eq, PartialEq)]
148#[non_exhaustive]
149pub struct QuadletParseFailure {
150 stage: QuadletParseFailureStage,
151 input_index: Option<usize>,
152 source_id: Option<u32>,
153 span: Option<QuadletParseDiagnosticLabel>,
154 summary: &'static str,
155}
156
157impl QuadletParseFailure {
158 #[must_use]
160 pub const fn stage(&self) -> QuadletParseFailureStage {
161 self.stage
162 }
163
164 #[must_use]
166 pub const fn input_index(&self) -> Option<usize> {
167 self.input_index
168 }
169
170 #[must_use]
172 pub const fn source_id(&self) -> Option<u32> {
173 self.source_id
174 }
175
176 #[must_use]
178 pub const fn span(&self) -> Option<&QuadletParseDiagnosticLabel> {
179 self.span.as_ref()
180 }
181
182 #[must_use]
184 pub const fn summary(&self) -> &'static str {
185 self.summary
186 }
187}
188
189#[derive(Clone, Debug, Eq, PartialEq)]
191#[non_exhaustive]
192pub struct QuadletParseResult {
193 source: QuadletSource,
194 diagnostics: Vec<QuadletParseDiagnostic>,
195}
196
197impl QuadletParseResult {
198 #[must_use]
200 pub const fn source(&self) -> &QuadletSource {
201 &self.source
202 }
203
204 #[must_use]
206 pub fn diagnostics(&self) -> &[QuadletParseDiagnostic] {
207 &self.diagnostics
208 }
209
210 #[must_use]
212 pub fn into_source(self) -> QuadletSource {
213 self.source
214 }
215}
216
217#[derive(Clone, Debug, Eq, PartialEq)]
219#[non_exhaustive]
220pub struct QuadletParseError {
221 diagnostics: Vec<QuadletParseDiagnostic>,
222 failures: Vec<QuadletParseFailure>,
223}
224
225impl QuadletParseError {
226 #[must_use]
228 pub fn diagnostics(&self) -> &[QuadletParseDiagnostic] {
229 &self.diagnostics
230 }
231
232 #[must_use]
234 pub fn failures(&self) -> &[QuadletParseFailure] {
235 &self.failures
236 }
237}
238
239impl fmt::Display for QuadletParseError {
240 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
241 formatter.write_str("Quadlet parse failed")
242 }
243}
244
245impl Error for QuadletParseError {}
246
247#[derive(Clone, Debug, Eq, PartialEq)]
249pub struct QuadletDocumentInput {
250 name: String,
251 source_id: QuadletSourceId,
252 text: String,
253}
254
255impl QuadletDocumentInput {
256 #[must_use]
258 pub fn new(name: impl Into<String>, source_id: QuadletSourceId, text: impl Into<String>) -> Self {
259 Self {
260 name: name.into(),
261 source_id,
262 text: text.into(),
263 }
264 }
265
266 #[must_use]
268 pub fn name(&self) -> &str {
269 &self.name
270 }
271
272 #[must_use]
274 pub const fn source_id(&self) -> QuadletSourceId {
275 self.source_id
276 }
277
278 #[must_use]
280 pub fn text(&self) -> &str {
281 &self.text
282 }
283}
284
285#[derive(Clone, Debug, Eq, PartialEq)]
287pub struct QuadletSource {
288 application_name: Identifier,
289 documents: QuadletDocumentSet,
290 source_ids: BTreeMap<QuadletSourceId, SourceId>,
291 fallback_source_id: SourceId,
292 native_findings: Vec<NativeFinding>,
293}
294
295impl QuadletSource {
296 pub fn parse(
312 application_name: Identifier,
313 inputs: impl IntoIterator<Item = QuadletDocumentInput>,
314 ) -> Result<QuadletParseResult, QuadletParseError> {
315 let mut documents = Vec::new();
316 let mut source_names = Vec::new();
317 let mut diagnostics = Vec::new();
318 let mut failures = Vec::new();
319
320 for (input_index, input) in inputs.into_iter().enumerate() {
321 let source_id = input.source_id;
322 let filename = match UnitFileName::new(input.name.clone()) {
323 Ok(filename) => filename,
324 Err(error) => {
325 failures.push(document_metadata_failure(
326 QuadletParseFailureStage::Filename,
327 Some(input_index),
328 Some(source_id.get()),
329 None,
330 &error,
331 ));
332 continue;
333 }
334 };
335 let parsed = match QuadletDocument::parse(filename.unit_type(), source_id, input.text) {
336 Ok(parsed) => parsed,
337 Err(error) => {
338 failures.push(typed_model_failure(input_index, source_id, error));
339 continue;
340 }
341 };
342 diagnostics.extend(copy_diagnostics(
343 QuadletParseDiagnosticOrigin::Syntax,
344 parsed.syntax().diagnostics(),
345 ));
346 diagnostics.extend(copy_diagnostics(
347 QuadletParseDiagnosticOrigin::Model,
348 parsed.model_diagnostics(),
349 ));
350 match NamedQuadletDocument::new(filename.as_str(), parsed.document().clone()) {
351 Ok(document) => {
352 source_names.push((source_id, filename.as_str().to_owned()));
353 documents.push(document);
354 }
355 Err(error) => failures.push(document_metadata_failure(
356 QuadletParseFailureStage::DocumentMetadata,
357 Some(input_index),
358 Some(source_id.get()),
359 None,
360 &error,
361 )),
362 }
363 }
364
365 let documents = match QuadletDocumentSet::new(documents) {
366 Ok(documents) => {
367 diagnostics.extend(copy_diagnostics(
368 QuadletParseDiagnosticOrigin::DocumentSet,
369 documents.diagnostics(),
370 ));
371 Some(documents)
372 }
373 Err(error) => {
374 failures.push(document_set_failure(&error));
375 None
376 }
377 };
378
379 if diagnostics.iter().any(|diagnostic| {
380 diagnostic.severity() == QuadletParseDiagnosticSeverity::Error
381 && !matches!(diagnostic.origin(), QuadletParseDiagnosticOrigin::DocumentSet)
382 }) || !failures.is_empty()
383 {
384 return Err(QuadletParseError { diagnostics, failures });
385 }
386
387 let Some(documents) = documents else {
388 return Err(QuadletParseError {
389 diagnostics,
390 failures: vec![QuadletParseFailure {
391 stage: QuadletParseFailureStage::DocumentSet,
392 input_index: None,
393 source_id: None,
394 span: None,
395 summary: "Quadlet document set could not be constructed",
396 }],
397 });
398 };
399 let mut source = match Self::from_validated_documents(application_name, documents) {
400 Ok(source) => source,
401 Err(error) => {
402 failures.push(neutral_model_failure(error));
403 return Err(QuadletParseError { diagnostics, failures });
404 }
405 };
406 for (native, name) in source_names {
407 let neutral = match SourceId::new(name) {
408 Ok(neutral) => neutral,
409 Err(error) => {
410 failures.push(neutral_model_failure(error));
411 return Err(QuadletParseError { diagnostics, failures });
412 }
413 };
414 source = source.with_source_id(native, neutral);
415 }
416 source.native_findings = diagnostics.iter().map(native_finding).collect();
417 Ok(QuadletParseResult { source, diagnostics })
418 }
419
420 pub fn from_validated_documents(
431 application_name: Identifier,
432 documents: QuadletDocumentSet,
433 ) -> Result<Self, ModelError> {
434 let fallback_source_id = SourceId::new("quadlet-source")?;
435 let native_findings = copy_diagnostics(QuadletParseDiagnosticOrigin::DocumentSet, documents.diagnostics())
436 .iter()
437 .map(native_finding)
438 .collect();
439 let source_ids = documents
440 .documents()
441 .iter()
442 .map(|document| document.document().source_id())
443 .map(|source_id| {
444 SourceId::new(format!("quadlet-source-{}", source_id.get())).map(|neutral| (source_id, neutral))
445 })
446 .collect::<Result<_, _>>()?;
447 Ok(Self {
448 application_name,
449 documents,
450 source_ids,
451 fallback_source_id,
452 native_findings,
453 })
454 }
455
456 #[must_use]
458 pub fn with_source_id(mut self, quadlet: QuadletSourceId, neutral: SourceId) -> Self {
459 self.source_ids.insert(quadlet, neutral);
460 self
461 }
462
463 #[must_use]
465 pub const fn application_name(&self) -> &Identifier {
466 &self.application_name
467 }
468
469 #[must_use]
471 pub const fn documents(&self) -> &QuadletDocumentSet {
472 &self.documents
473 }
474
475 #[must_use]
477 pub fn source_id(&self, quadlet: QuadletSourceId) -> &SourceId {
478 self.source_ids.get(&quadlet).unwrap_or(&self.fallback_source_id)
479 }
480
481 #[must_use]
486 pub fn native_findings(&self) -> &[NativeFinding] {
487 &self.native_findings
488 }
489}
490
491fn native_finding(diagnostic: &QuadletParseDiagnostic) -> NativeFinding {
492 let mut finding = NativeFinding::new(
493 "quadlet",
494 "quadlet-lens",
495 diagnostic.code(),
496 match diagnostic.origin() {
497 QuadletParseDiagnosticOrigin::Syntax => "syntax",
498 QuadletParseDiagnosticOrigin::Model => "model",
499 QuadletParseDiagnosticOrigin::DocumentSet => "document-set",
500 },
501 match diagnostic.severity() {
502 QuadletParseDiagnosticSeverity::Error => Severity::Error,
503 QuadletParseDiagnosticSeverity::Warning => Severity::Warning,
504 QuadletParseDiagnosticSeverity::Note => Severity::Note,
505 },
506 diagnostic.summary(),
507 );
508 for label in diagnostic.labels() {
509 finding = finding.with_label(NativeFindingLabel::new(
510 NativeFindingLabelKind::Primary,
511 label.source_id(),
512 label.start(),
513 label.end(),
514 label.message(),
515 ));
516 }
517 finding
518}
519
520fn copy_diagnostics(
521 origin: QuadletParseDiagnosticOrigin,
522 diagnostics: &[NativeDiagnostic],
523) -> Vec<QuadletParseDiagnostic> {
524 diagnostics
525 .iter()
526 .map(|diagnostic| QuadletParseDiagnostic {
527 code: diagnostic.code().as_str(),
528 severity: match diagnostic.severity() {
529 NativeSeverity::Error => QuadletParseDiagnosticSeverity::Error,
530 NativeSeverity::Warning => QuadletParseDiagnosticSeverity::Warning,
531 NativeSeverity::Note => QuadletParseDiagnosticSeverity::Note,
532 },
533 summary: diagnostic.summary(),
534 origin,
535 labels: diagnostic.labels().iter().map(diagnostic_label).collect(),
536 })
537 .collect()
538}
539
540fn diagnostic_label(label: &quadlet_lens::diagnostic::Label) -> QuadletParseDiagnosticLabel {
541 span_label(label.span(), label.message())
542}
543
544fn span_label(span: QuadletSourceSpan, message: &'static str) -> QuadletParseDiagnosticLabel {
545 QuadletParseDiagnosticLabel {
546 source_id: span.source_id().get(),
547 start: span.start(),
548 end: span.end(),
549 message,
550 }
551}
552
553fn typed_model_failure(input_index: usize, source_id: QuadletSourceId, error: TypedModelError) -> QuadletParseFailure {
554 match error {
555 TypedModelError::InvalidSourceSpan(span) => QuadletParseFailure {
556 stage: QuadletParseFailureStage::TypedModel,
557 input_index: Some(input_index),
558 source_id: Some(source_id.get()),
559 span: Some(span_label(span, "native typed-model span could not be resolved")),
560 summary: "Quadlet typed-model construction failed",
561 },
562 _ => QuadletParseFailure {
563 stage: QuadletParseFailureStage::TypedModel,
564 input_index: Some(input_index),
565 source_id: Some(source_id.get()),
566 span: None,
567 summary: "Quadlet typed-model construction failed",
568 },
569 }
570}
571
572fn document_metadata_failure(
573 stage: QuadletParseFailureStage,
574 input_index: Option<usize>,
575 source_id: Option<u32>,
576 span: Option<QuadletParseDiagnosticLabel>,
577 error: &DocumentSetError,
578) -> QuadletParseFailure {
579 let summary = match error {
580 DocumentSetError::InvalidUnitFileName(_) => "Quadlet unit filename is invalid",
581 DocumentSetError::UnsupportedUnitFileExtension(_) => "Quadlet unit filename has an unsupported extension",
582 DocumentSetError::UnitTypeMismatch { .. } => "Quadlet unit filename and typed document kind differ",
583 DocumentSetError::DuplicateSourceId(_) => "Quadlet named document has a duplicate source identity",
584 _ => "Quadlet native document metadata could not be constructed",
585 };
586 QuadletParseFailure {
587 stage,
588 input_index,
589 source_id,
590 span,
591 summary,
592 }
593}
594
595fn document_set_failure(error: &DocumentSetError) -> QuadletParseFailure {
596 let source_id = match error {
597 DocumentSetError::DuplicateSourceId(source_id) => Some(source_id.get()),
598 _ => None,
599 };
600 let summary = match error {
601 DocumentSetError::InvalidUnitFileName(_) => "Quadlet document set contains an invalid unit filename",
602 DocumentSetError::UnsupportedUnitFileExtension(_) => {
603 "Quadlet document set contains an unsupported unit extension"
604 }
605 DocumentSetError::UnitTypeMismatch { .. } => "Quadlet document set has mismatched document metadata",
606 DocumentSetError::DuplicateSourceId(_) => "Quadlet document set has duplicate source identities",
607 _ => "Quadlet document set could not be constructed",
608 };
609 QuadletParseFailure {
610 stage: QuadletParseFailureStage::DocumentSet,
611 input_index: None,
612 source_id,
613 span: None,
614 summary,
615 }
616}
617
618fn neutral_model_failure(_error: ModelError) -> QuadletParseFailure {
619 QuadletParseFailure {
620 stage: QuadletParseFailureStage::NeutralModel,
621 input_index: None,
622 source_id: None,
623 span: None,
624 summary: "BoxFerry neutral Quadlet source construction failed",
625 }
626}