1use crate::units::Dimension;
2#[cfg(feature = "python")]
3use pyo3::{IntoPyObject, PyAny, Python};
4use std::fmt;
5
6pub const FUZZY_MATCH_THRESHOLD: usize = 2;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub struct Position {
12 pub line: usize,
13 pub column: usize,
14}
15
16impl Position {
17 pub fn new(line: usize, column: usize) -> Result<Self, String> {
18 if line == 0 || column == 0 {
19 Err(format!(
20 "Position line and column must be >= 1. Got line={}, column={}",
21 line, column
22 ))
23 } else {
24 Ok(Self { line, column })
25 }
26 }
27}
28
29impl fmt::Display for Position {
30 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31 write!(f, "{}:{}", self.line, self.column)
32 }
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub struct SourceRange {
38 pub start: Position,
39 pub end: Position,
40}
41
42impl SourceRange {
43 pub fn new(start: Position, end: Position) -> Self {
44 Self { start, end }
45 }
46
47 pub fn from_line_col(
48 start_line: usize,
49 start_col: usize,
50 end_line: usize,
51 end_col: usize,
52 ) -> Result<Self, String> {
53 Ok(Self {
54 start: Position::new(start_line, start_col)?,
55 end: Position::new(end_line, end_col)?,
56 })
57 }
58
59 pub fn single_position(line: usize, column: usize) -> Result<Self, String> {
60 let pos = Position::new(line, column)?;
61 Ok(Self {
62 start: pos,
63 end: pos,
64 })
65 }
66}
67
68impl fmt::Display for SourceRange {
69 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70 if self.start == self.end {
71 write!(f, "{}", self.start)
72 } else if self.start.line == self.end.line {
73 write!(
74 f,
75 "{}:{}-{}",
76 self.start.line, self.start.column, self.end.column
77 )
78 } else {
79 write!(f, "{} to {}", self.start, self.end)
80 }
81 }
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86#[allow(non_camel_case_types)]
87pub enum ErrorCode {
88 E001_UndefinedEntity,
90 E002_UndefinedResource,
91 E003_UnitMismatch,
92 E004_TypeMismatch,
93 E005_SyntaxError,
94 E006_InvalidExpression,
95 E007_DuplicateDeclaration,
96 E008_UndefinedVariable,
97 E009_InvalidQuantity,
98 E010_InvalidIdentifier,
99
100 E100_IncompatibleTypes,
102 E101_InvalidTypeConversion,
103 E102_TypeInferenceFailed,
104 E103_InvalidOperandType,
105 E104_InvalidComparisonType,
106
107 E200_DimensionMismatch,
109 E201_InvalidUnit,
110 E202_UnitConversionFailed,
111 E203_IncompatibleDimensions,
112
113 E300_VariableNotInScope,
115 E301_UndefinedReference,
116 E302_CircularReference,
117 E303_InvalidReference,
118
119 E400_PolicyEvaluationFailed,
121 E401_InvalidPolicyExpression,
122 E402_DeterminismViolation,
123 E403_InvalidModality,
124
125 E500_NamespaceNotFound,
127 E501_AmbiguousNamespace,
128 E502_InvalidNamespace,
129 E503_ModuleNotFound,
130 E504_SymbolNotExported,
131 E505_CircularDependency,
132 E506_AmbiguousImport,
133 E507_InvalidExport,
134}
135
136impl ErrorCode {
137 pub fn as_str(&self) -> &'static str {
138 match self {
139 ErrorCode::E001_UndefinedEntity => "E001",
141 ErrorCode::E002_UndefinedResource => "E002",
142 ErrorCode::E003_UnitMismatch => "E003",
143 ErrorCode::E004_TypeMismatch => "E004",
144 ErrorCode::E005_SyntaxError => "E005",
145 ErrorCode::E006_InvalidExpression => "E006",
146 ErrorCode::E007_DuplicateDeclaration => "E007",
147 ErrorCode::E008_UndefinedVariable => "E008",
148 ErrorCode::E009_InvalidQuantity => "E009",
149 ErrorCode::E010_InvalidIdentifier => "E010",
150
151 ErrorCode::E100_IncompatibleTypes => "E100",
153 ErrorCode::E101_InvalidTypeConversion => "E101",
154 ErrorCode::E102_TypeInferenceFailed => "E102",
155 ErrorCode::E103_InvalidOperandType => "E103",
156 ErrorCode::E104_InvalidComparisonType => "E104",
157
158 ErrorCode::E200_DimensionMismatch => "E200",
160 ErrorCode::E201_InvalidUnit => "E201",
161 ErrorCode::E202_UnitConversionFailed => "E202",
162 ErrorCode::E203_IncompatibleDimensions => "E203",
163
164 ErrorCode::E300_VariableNotInScope => "E300",
166 ErrorCode::E301_UndefinedReference => "E301",
167 ErrorCode::E302_CircularReference => "E302",
168 ErrorCode::E303_InvalidReference => "E303",
169
170 ErrorCode::E400_PolicyEvaluationFailed => "E400",
172 ErrorCode::E401_InvalidPolicyExpression => "E401",
173 ErrorCode::E402_DeterminismViolation => "E402",
174 ErrorCode::E403_InvalidModality => "E403",
175
176 ErrorCode::E500_NamespaceNotFound => "E500",
178 ErrorCode::E501_AmbiguousNamespace => "E501",
179 ErrorCode::E502_InvalidNamespace => "E502",
180 ErrorCode::E503_ModuleNotFound => "E503",
181 ErrorCode::E504_SymbolNotExported => "E504",
182 ErrorCode::E505_CircularDependency => "E505",
183 ErrorCode::E506_AmbiguousImport => "E506",
184 ErrorCode::E507_InvalidExport => "E507",
185 }
186 }
187
188 pub fn description(&self) -> &'static str {
189 match self {
190 ErrorCode::E001_UndefinedEntity => "Undefined entity",
191 ErrorCode::E002_UndefinedResource => "Undefined resource",
192 ErrorCode::E003_UnitMismatch => "Unit mismatch",
193 ErrorCode::E004_TypeMismatch => "Type mismatch",
194 ErrorCode::E005_SyntaxError => "Syntax error",
195 ErrorCode::E006_InvalidExpression => "Invalid expression",
196 ErrorCode::E007_DuplicateDeclaration => "Duplicate declaration",
197 ErrorCode::E008_UndefinedVariable => "Undefined variable",
198 ErrorCode::E009_InvalidQuantity => "Invalid quantity",
199 ErrorCode::E010_InvalidIdentifier => "Invalid identifier",
200 ErrorCode::E100_IncompatibleTypes => "Incompatible types",
201 ErrorCode::E101_InvalidTypeConversion => "Invalid type conversion",
202 ErrorCode::E102_TypeInferenceFailed => "Type inference failed",
203 ErrorCode::E103_InvalidOperandType => "Invalid operand type",
204 ErrorCode::E104_InvalidComparisonType => "Invalid comparison type",
205 ErrorCode::E200_DimensionMismatch => "Dimension mismatch",
206 ErrorCode::E201_InvalidUnit => "Invalid unit",
207 ErrorCode::E202_UnitConversionFailed => "Unit conversion failed",
208 ErrorCode::E203_IncompatibleDimensions => "Incompatible dimensions",
209 ErrorCode::E300_VariableNotInScope => "Variable not in scope",
210 ErrorCode::E301_UndefinedReference => "Undefined reference",
211 ErrorCode::E302_CircularReference => "Circular reference",
212 ErrorCode::E303_InvalidReference => "Invalid reference",
213 ErrorCode::E400_PolicyEvaluationFailed => "Policy evaluation failed",
214 ErrorCode::E401_InvalidPolicyExpression => "Invalid policy expression",
215 ErrorCode::E402_DeterminismViolation => "Determinism violation",
216 ErrorCode::E403_InvalidModality => "Invalid modality",
217 ErrorCode::E500_NamespaceNotFound => "Namespace not found",
218 ErrorCode::E501_AmbiguousNamespace => "Ambiguous namespace",
219 ErrorCode::E502_InvalidNamespace => "Invalid namespace",
220 ErrorCode::E503_ModuleNotFound => "Module not found",
221 ErrorCode::E504_SymbolNotExported => "Imported symbol is not exported",
222 ErrorCode::E505_CircularDependency => "Circular dependency detected",
223 ErrorCode::E506_AmbiguousImport => "Ambiguous import",
224 ErrorCode::E507_InvalidExport => "Invalid export",
225 }
226 }
227}
228
229impl fmt::Display for ErrorCode {
230 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
231 write!(f, "{}", self.as_str())
232 }
233}
234
235#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
236pub enum ReferenceType {
237 Entity,
238 Resource,
239 Variable,
240 Flow,
241 Other(String),
242}
243
244impl fmt::Display for ReferenceType {
245 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
246 match self {
247 ReferenceType::Entity => write!(f, "Entity"),
248 ReferenceType::Resource => write!(f, "Resource"),
249 ReferenceType::Variable => write!(f, "Variable"),
250 ReferenceType::Flow => write!(f, "Flow"),
251 ReferenceType::Other(s) => write!(f, "{}", s),
252 }
253 }
254}
255
256#[cfg(feature = "python")]
257impl<'py> IntoPyObject<'py> for ReferenceType {
258 type Target = PyAny;
259 type Output = pyo3::Bound<'py, PyAny>;
260 type Error = pyo3::PyErr;
261
262 fn into_pyobject(self, py: Python<'py>) -> pyo3::PyResult<Self::Output> {
263 Ok(pyo3::types::PyString::new(py, &self.to_string()).into_any())
264 }
265}
266
267#[derive(Debug, Clone)]
268pub enum ValidationError {
269 SyntaxError {
270 message: String,
271 line: usize,
272 column: usize,
273 end_line: Option<usize>,
274 end_column: Option<usize>,
275 },
276 TypeError {
277 message: String,
278 location: String,
279 expected_type: Option<String>,
280 found_type: Option<String>,
281 suggestion: Option<String>,
282 },
283 UnitError {
284 expected: Dimension,
285 found: Dimension,
286 location: String,
287 suggestion: Option<String>,
288 },
289 ScopeError {
290 variable: String,
291 available_in: Vec<String>,
292 location: String,
293 suggestion: Option<String>,
294 },
295 DeterminismError {
296 message: String,
297 hint: String,
298 },
299 UndefinedReference {
300 reference_type: ReferenceType,
301 name: String,
302 location: String,
303 suggestion: Option<String>,
304 },
305 DuplicateDeclaration {
306 name: String,
307 first_location: String,
308 second_location: String,
309 },
310 InvalidExpression {
311 message: String,
312 location: String,
313 suggestion: Option<String>,
314 },
315}
316
317impl ValidationError {
318 pub fn error_code(&self) -> ErrorCode {
320 match self {
321 ValidationError::SyntaxError { .. } => ErrorCode::E005_SyntaxError,
322 ValidationError::TypeError { .. } => ErrorCode::E004_TypeMismatch,
323 ValidationError::UnitError { .. } => ErrorCode::E003_UnitMismatch,
324 ValidationError::ScopeError { .. } => ErrorCode::E300_VariableNotInScope,
325 ValidationError::DeterminismError { .. } => ErrorCode::E402_DeterminismViolation,
326 ValidationError::UndefinedReference { reference_type, .. } => match reference_type {
327 ReferenceType::Entity => ErrorCode::E001_UndefinedEntity,
328 ReferenceType::Resource => ErrorCode::E002_UndefinedResource,
329 ReferenceType::Variable => ErrorCode::E008_UndefinedVariable,
330 _ => ErrorCode::E301_UndefinedReference,
331 },
332 ValidationError::DuplicateDeclaration { .. } => ErrorCode::E007_DuplicateDeclaration,
333 ValidationError::InvalidExpression { .. } => ErrorCode::E006_InvalidExpression,
334 }
335 }
336
337 pub fn range(&self) -> Option<SourceRange> {
339 match self {
340 ValidationError::SyntaxError {
341 line,
342 column,
343 end_line,
344 end_column,
345 ..
346 } => {
347 let start =
349 Position::new(*line, *column).unwrap_or_else(|_| Position::new(1, 1).unwrap());
350 let end = match (end_line, end_column) {
351 (Some(el), Some(ec)) => Position::new(*el, *ec).unwrap_or(start),
352 _ => start,
353 };
354 Some(SourceRange::new(start, end))
355 }
356 _ => None, }
358 }
359
360 pub fn location_string(&self) -> Option<String> {
362 match self {
363 ValidationError::SyntaxError { line, column, .. } => {
364 Some(format!("{}:{}", line, column))
365 }
366 ValidationError::TypeError { location, .. }
367 | ValidationError::UnitError { location, .. }
368 | ValidationError::ScopeError { location, .. }
369 | ValidationError::UndefinedReference { location, .. }
370 | ValidationError::InvalidExpression { location, .. } => Some(location.clone()),
371 ValidationError::DuplicateDeclaration {
372 second_location, ..
373 } => Some(second_location.clone()),
374 ValidationError::DeterminismError { .. } => None,
375 }
376 }
377
378 pub fn syntax_error(message: impl Into<String>, line: usize, column: usize) -> Self {
379 Self::SyntaxError {
380 message: message.into(),
381 line,
382 column,
383 end_line: None,
384 end_column: None,
385 }
386 }
387
388 pub fn syntax_error_with_range(
389 message: impl Into<String>,
390 line: usize,
391 column: usize,
392 end_line: usize,
393 end_column: usize,
394 ) -> Self {
395 Self::SyntaxError {
396 message: message.into(),
397 line,
398 column,
399 end_line: Some(end_line),
400 end_column: Some(end_column),
401 }
402 }
403
404 pub fn type_error(message: impl Into<String>, location: impl Into<String>) -> Self {
405 Self::TypeError {
406 message: message.into(),
407 location: location.into(),
408 expected_type: None,
409 found_type: None,
410 suggestion: None,
411 }
412 }
413
414 pub fn unit_error(expected: Dimension, found: Dimension, location: impl Into<String>) -> Self {
415 Self::UnitError {
416 expected,
417 found,
418 location: location.into(),
419 suggestion: None,
420 }
421 }
422
423 pub fn scope_error(
424 variable: impl Into<String>,
425 available_in: Vec<String>,
426 location: impl Into<String>,
427 ) -> Self {
428 Self::ScopeError {
429 variable: variable.into(),
430 available_in,
431 location: location.into(),
432 suggestion: None,
433 }
434 }
435
436 pub fn determinism_error(message: impl Into<String>, hint: impl Into<String>) -> Self {
437 Self::DeterminismError {
438 message: message.into(),
439 hint: hint.into(),
440 }
441 }
442
443 pub fn undefined_reference(
444 reference_type: impl Into<String>,
445 name: impl Into<String>,
446 location: impl Into<String>,
447 ) -> Self {
448 Self::UndefinedReference {
449 reference_type: ReferenceType::Other(reference_type.into()),
450 name: name.into(),
451 location: location.into(),
452 suggestion: None,
453 }
454 }
455
456 pub fn duplicate_declaration(
457 name: impl Into<String>,
458 first_location: impl Into<String>,
459 second_location: impl Into<String>,
460 ) -> Self {
461 Self::DuplicateDeclaration {
462 name: name.into(),
463 first_location: first_location.into(),
464 second_location: second_location.into(),
465 }
466 }
467
468 pub fn invalid_expression(message: impl Into<String>, location: impl Into<String>) -> Self {
469 Self::InvalidExpression {
470 message: message.into(),
471 location: location.into(),
472 suggestion: None,
473 }
474 }
475
476 pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
477 match &mut self {
478 ValidationError::UnitError { suggestion: s, .. } => {
479 *s = Some(suggestion.into());
480 }
481 ValidationError::TypeError { suggestion: s, .. } => {
482 *s = Some(suggestion.into());
483 }
484 ValidationError::ScopeError { suggestion: s, .. } => {
485 *s = Some(suggestion.into());
486 }
487 ValidationError::UndefinedReference { suggestion: s, .. } => {
488 *s = Some(suggestion.into());
489 }
490 ValidationError::InvalidExpression { suggestion: s, .. } => {
491 *s = Some(suggestion.into());
492 }
493 _ => {}
494 }
495 self
496 }
497
498 pub fn with_types(
499 mut self,
500 expected_type: impl Into<String>,
501 found_type: impl Into<String>,
502 ) -> Self {
503 if let ValidationError::TypeError {
504 expected_type: e,
505 found_type: f,
506 ..
507 } = &mut self
508 {
509 *e = Some(expected_type.into());
510 *f = Some(found_type.into());
511 }
512 self
513 }
514
515 pub fn undefined_entity(name: impl Into<String>, location: impl Into<String>) -> Self {
519 let name = name.into();
520 Self::UndefinedReference {
521 reference_type: ReferenceType::Entity,
522 name: name.clone(),
523 location: location.into(),
524 suggestion: Some(format!("Did you mean to define 'Entity \"{}\"'?", name)),
525 }
526 }
527
528 pub fn undefined_resource(name: impl Into<String>, location: impl Into<String>) -> Self {
530 let name = name.into();
531 Self::UndefinedReference {
532 reference_type: ReferenceType::Resource,
533 name: name.clone(),
534 location: location.into(),
535 suggestion: Some(format!("Did you mean to define 'Resource \"{}\"'?", name)),
536 }
537 }
538
539 pub fn undefined_flow(name: impl Into<String>, location: impl Into<String>) -> Self {
541 let name = name.into();
542 Self::UndefinedReference {
543 reference_type: ReferenceType::Flow,
544 name: name.clone(),
545 location: location.into(),
546 suggestion: Some(format!(
547 "Did you mean to define a Flow involving '{}'?",
548 name
549 )),
550 }
551 }
552
553 pub fn unit_mismatch(
555 expected: Dimension,
556 found: Dimension,
557 location: impl Into<String>,
558 ) -> Self {
559 let suggestion = Some(format!(
560 "Expected dimension {:?} but found {:?}. Consider using unit conversion or checking your unit definitions.",
561 expected, found
562 ));
563 Self::UnitError {
564 expected,
565 found,
566 location: location.into(),
567 suggestion,
568 }
569 }
570
571 pub fn type_mismatch(
573 expected: impl Into<String>,
574 found: impl Into<String>,
575 location: impl Into<String>,
576 ) -> Self {
577 let expected = expected.into();
578 let found = found.into();
579 Self::TypeError {
580 message: format!("Type mismatch: expected {}, found {}", expected, found),
581 location: location.into(),
582 expected_type: Some(expected.clone()),
583 found_type: Some(found.clone()),
584 suggestion: Some(format!(
585 "Convert {} to {} or adjust the expression",
586 found, expected
587 )),
588 }
589 }
590
591 pub fn variable_not_in_scope(
593 variable: impl Into<String>,
594 available: Vec<String>,
595 location: impl Into<String>,
596 ) -> Self {
597 let variable = variable.into();
598 let suggestion = if !available.is_empty() {
599 Some(format!(
600 "Available variables: {}. Did you mean one of these?",
601 available.join(", ")
602 ))
603 } else {
604 Some("No variables are currently in scope.".to_string())
605 };
606
607 Self::ScopeError {
608 variable,
609 available_in: available,
610 location: location.into(),
611 suggestion,
612 }
613 }
614
615 fn undefined_reference_with_candidates(
622 reference_type: ReferenceType,
623 name: String,
624 location: String,
625 candidates: &[String],
626 ) -> Self {
627 use crate::error::fuzzy::find_best_match;
628
629 let suggestion = find_best_match(&name, candidates, FUZZY_MATCH_THRESHOLD)
630 .map(|match_name| format!("Did you mean '{}'?", match_name))
631 .or_else(|| {
632 Some(format!(
633 "Did you mean to define '{} \"{}\"'?",
634 reference_type, name
635 ))
636 });
637
638 Self::UndefinedReference {
639 reference_type,
640 name,
641 location,
642 suggestion,
643 }
644 }
645
646 pub fn undefined_entity_with_candidates(
653 name: impl Into<String>,
654 location: impl Into<String>,
655 candidates: &[String],
656 ) -> Self {
657 Self::undefined_reference_with_candidates(
658 ReferenceType::Entity,
659 name.into(),
660 location.into(),
661 candidates,
662 )
663 }
664
665 pub fn undefined_resource_with_candidates(
667 name: impl Into<String>,
668 location: impl Into<String>,
669 candidates: &[String],
670 ) -> Self {
671 Self::undefined_reference_with_candidates(
672 ReferenceType::Resource,
673 name.into(),
674 location.into(),
675 candidates,
676 )
677 }
678
679 pub fn undefined_variable_with_candidates(
681 name: impl Into<String>,
682 location: impl Into<String>,
683 candidates: &[String],
684 ) -> Self {
685 use crate::error::fuzzy::suggest_similar;
686
687 let name = name.into();
688 let matches = suggest_similar(&name, candidates, FUZZY_MATCH_THRESHOLD);
689 let suggestion = if !matches.is_empty() {
690 let quoted: Vec<String> = matches.iter().map(|m| format!("'{}'", m)).collect();
691 Some(format!("Did you mean {}?", quoted.join(", ")))
692 } else {
693 Some("No similar variables found in scope.".to_string())
694 };
695
696 Self::UndefinedReference {
697 reference_type: ReferenceType::Variable,
698 name,
699 location: location.into(),
700 suggestion,
701 }
702 }
703}
704
705impl fmt::Display for ValidationError {
706 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
707 match self {
708 ValidationError::SyntaxError {
709 message,
710 line,
711 column,
712 end_line,
713 end_column,
714 } => {
715 if let (Some(el), Some(ec)) = (end_line, end_column) {
716 write!(
717 f,
718 "Syntax error at {}:{} to {}:{}: {}",
719 line, column, el, ec, message
720 )
721 } else {
722 write!(f, "Syntax error at {}:{}: {}", line, column, message)
723 }
724 }
725 ValidationError::TypeError {
726 message,
727 location,
728 expected_type,
729 found_type,
730 suggestion,
731 } => {
732 write!(f, "Type error at {}: {}", location, message)?;
733 if let (Some(exp), Some(fnd)) = (expected_type, found_type) {
734 write!(f, " (expected {}, found {})", exp, fnd)?;
735 }
736 if let Some(sug) = suggestion {
737 write!(f, "\n Suggestion: {}", sug)?;
738 }
739 Ok(())
740 }
741 ValidationError::UnitError {
742 expected,
743 found,
744 location,
745 suggestion,
746 } => {
747 write!(
748 f,
749 "Unit error at {}: incompatible dimensions (expected {:?}, found {:?})",
750 location, expected, found
751 )?;
752 if let Some(sug) = suggestion {
753 write!(f, "\n Suggestion: {}", sug)?;
754 }
755 Ok(())
756 }
757 ValidationError::ScopeError {
758 variable,
759 available_in,
760 location,
761 suggestion,
762 } => {
763 write!(
764 f,
765 "Scope error at {}: variable '{}' not in scope",
766 location, variable
767 )?;
768 if !available_in.is_empty() {
769 write!(f, "\n Available in: {}", available_in.join(", "))?;
770 }
771 if let Some(sug) = suggestion {
772 write!(f, "\n Suggestion: {}", sug)?;
773 }
774 Ok(())
775 }
776 ValidationError::DeterminismError { message, hint } => {
777 write!(f, "Determinism error: {}", message)?;
778 write!(f, "\n Hint: {}", hint)
779 }
780 ValidationError::UndefinedReference {
781 reference_type,
782 name,
783 location,
784 suggestion,
785 } => {
786 write!(f, "Undefined {} '{}' at {}", reference_type, name, location)?;
787 if let Some(sug) = suggestion {
788 write!(f, "\n Suggestion: {}", sug)?;
789 }
790 Ok(())
791 }
792 ValidationError::DuplicateDeclaration {
793 name,
794 first_location,
795 second_location,
796 } => {
797 write!(
798 f,
799 "Duplicate declaration of '{}': first at {}, duplicate at {}",
800 name, first_location, second_location
801 )
802 }
803 ValidationError::InvalidExpression {
804 message,
805 location,
806 suggestion,
807 } => {
808 write!(f, "Invalid expression at {}: {}", location, message)?;
809 if let Some(sug) = suggestion {
810 write!(f, "\n Suggestion: {}", sug)?;
811 }
812 Ok(())
813 }
814 }
815 }
816}
817
818impl std::error::Error for ValidationError {}