1use crate::proto::ast::*;
13use crate::proto::builtin_nodes::get_builtin_schema;
14use crate::proto::span::Span;
15use std::collections::HashMap;
16
17#[derive(Debug, Clone)]
19pub struct SchemaField {
20 pub name: String,
21 pub field_type: FieldType,
22}
23
24#[derive(Debug, Clone)]
26pub struct NodeSchema {
27 pub name: &'static str,
28 pub fields: Vec<SchemaField>,
29}
30
31impl NodeSchema {
32 pub fn get_field_type(&self, name: &str) -> Option<FieldType> {
33 self.fields
34 .iter()
35 .find(|field| field.name == name)
36 .map(|field| field.field_type.clone())
37 }
38}
39
40#[derive(Debug, Clone, PartialEq, Eq, Hash)]
42pub struct Diagnostic {
43 pub span: Span,
45 pub severity: Severity,
47 pub message: String,
49 pub suggestion: Option<String>,
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
55pub enum Severity {
56 Error,
58 Warning,
60}
61
62#[derive(Debug, Clone, Default)]
64pub struct DiagnosticSet {
65 diagnostics: Vec<Diagnostic>,
66}
67
68impl DiagnosticSet {
69 pub fn new() -> Self {
71 Self::default()
72 }
73
74 pub fn add(&mut self, diagnostic: Diagnostic) {
80 self.diagnostics.push(diagnostic);
81 }
82
83 pub fn extend(&mut self, diagnostics: impl IntoIterator<Item = Diagnostic>) {
85 self.diagnostics.extend(diagnostics);
86 }
87
88 pub fn iter(&self) -> impl Iterator<Item = &Diagnostic> {
90 self.diagnostics.iter()
91 }
92
93 pub fn has_errors(&self) -> bool {
95 self.diagnostics
96 .iter()
97 .any(|d| matches!(d.severity, Severity::Error))
98 }
99
100 pub fn len(&self) -> usize {
102 self.diagnostics.len()
103 }
104
105 pub fn is_empty(&self) -> bool {
107 self.diagnostics.is_empty()
108 }
109}
110
111pub(crate) fn validate_document(document: &Proto) -> DiagnosticSet {
112 let mut diagnostics = DiagnosticSet::new();
113 let mut context = ValidationContext::new();
114
115 diagnostics.extend(validate_header(document));
116
117 if let Some(proto_def) = &document.proto {
118 context.interface_fields = proto_def.fields.clone();
119 diagnostics.extend(validate_interface(proto_def));
120 diagnostics.extend(validate_node_body(&proto_def.body, &mut context));
121 }
122
123 for node in &document.root_nodes {
124 diagnostics.extend(validate_ast_node(node, &mut context));
125 }
126
127 diagnostics
128}
129
130pub(crate) fn validate_runtime_semantics(node: &AstNode) -> DiagnosticSet {
134 let mut diagnostics = DiagnosticSet::new();
135 validate_runtime_semantics_recursive(node, &mut diagnostics);
136 diagnostics
137}
138
139#[derive(Debug, Clone, Default)]
141pub struct ValidationContext {
142 pub defs: HashMap<String, String>, pub interface_fields: Vec<ProtoField>,
144}
145
146impl ValidationContext {
147 pub fn new() -> Self {
148 Self::default()
149 }
150}
151
152fn validate_header(document: &Proto) -> DiagnosticSet {
154 let mut diagnostics = DiagnosticSet::new();
155
156 if let Some(header) = &document.header {
157 if header.version != "R2025a" {
159 diagnostics.add(Diagnostic {
160 span: header.span.clone(),
161 severity: Severity::Error,
162 message: format!("Unsupported version: {}", header.version),
163 suggestion: Some("Use R2025a".to_string()),
164 });
165 }
166
167 if header.encoding != "utf8" {
169 diagnostics.add(Diagnostic {
170 span: header.span.clone(),
171 severity: Severity::Error,
172 message: format!("Unsupported encoding: {}", header.encoding),
173 suggestion: Some("Use utf8".to_string()),
174 });
175 }
176 } else if document.proto.is_some() || !document.root_nodes.is_empty() {
177 diagnostics.add(Diagnostic {
178 span: Span::default(),
179 severity: Severity::Error,
180 message: "Missing PROTO header".to_string(),
181 suggestion: Some("Add #VRML_SIM R2025a utf8 at the beginning of the file".to_string()),
182 });
183 }
184
185 diagnostics
186}
187
188fn validate_interface(proto_def: &ProtoDefinition) -> DiagnosticSet {
190 let mut diagnostics = DiagnosticSet::new();
191
192 for field in &proto_def.fields {
193 if let Some(default_value) = &field.default_value {
194 diagnostics.extend(validate_field_value(
195 default_value,
196 &field.field_type,
197 &field.span,
198 ));
199
200 if let Some(restrictions) = &field.restrictions {
201 if !restrictions
203 .iter()
204 .any(|value| values_are_equal(value, default_value))
205 {
206 diagnostics.add(Diagnostic {
207 span: field.span.clone(),
208 severity: Severity::Error,
209 message: format!(
210 "Default value {:?} is not in the allowed restrictions",
211 default_value
212 ),
213 suggestion: None,
214 });
215 }
216 }
217 }
218
219 if let Some(restrictions) = &field.restrictions {
220 for res in restrictions {
221 diagnostics.extend(validate_field_value(res, &field.field_type, &field.span));
222 }
223 }
224 }
225
226 diagnostics
227}
228
229fn values_are_equal(a: &FieldValue, b: &FieldValue) -> bool {
230 if let (FieldValue::Int(ia, _), FieldValue::Float(fb, _)) = (a, b) {
231 *ia as f64 == *fb
232 } else if let (FieldValue::Float(fa, _), FieldValue::Int(ib, _)) = (a, b) {
233 *fa == *ib as f64
234 } else {
235 a == b
236 }
237}
238
239fn validate_node_body(items: &[ProtoBodyItem], context: &mut ValidationContext) -> DiagnosticSet {
241 let mut diagnostics = DiagnosticSet::new();
242
243 for item in items {
244 match item {
245 ProtoBodyItem::Node(node) => {
246 diagnostics.extend(validate_ast_node(node, context));
247 }
248 ProtoBodyItem::Template(_) => {
249 }
251 }
252 }
253
254 diagnostics
255}
256
257fn validate_ast_node(node: &AstNode, context: &mut ValidationContext) -> DiagnosticSet {
259 let mut diagnostics = DiagnosticSet::new();
260
261 match &node.kind {
262 AstNodeKind::Node {
263 type_name,
264 def_name,
265 fields,
266 } => {
267 if let Some(name) = def_name {
268 context.defs.insert(name.clone(), type_name.clone());
269 }
270
271 diagnostics.extend(validate_runtime_semantics_for_node(node));
272
273 if let Some(schema) = get_builtin_schema(type_name) {
274 for element in fields {
275 if let NodeBodyElement::Field(field) = element {
276 if let Some(expected_type) = schema.get_field_type(&field.name) {
278 diagnostics.extend(validate_field_value(
279 &field.value,
280 &expected_type,
281 &field.span,
282 ));
283 diagnostics.extend(validate_node_field_semantics(
284 type_name,
285 &field.name,
286 &field.value,
287 &field.span,
288 ));
289
290 if let FieldValue::Is(ref_name) = &field.value {
292 if let Some(interface_field) = context
293 .interface_fields
294 .iter()
295 .find(|f| f.name == *ref_name)
296 {
297 if !types_are_compatible(
298 &interface_field.field_type,
299 &expected_type,
300 ) {
301 diagnostics.add(Diagnostic {
302 span: field.span.clone(),
303 severity: Severity::Error,
304 message: format!(
305 "Type mismatch in IS binding: interface field '{}' is {}, but node field '{}' expects {}",
306 ref_name,
307 field_type_name(&interface_field.field_type),
308 field.name,
309 field_type_name(&expected_type)
310 ),
311 suggestion: None,
312 });
313 }
314 } else {
315 diagnostics.add(Diagnostic {
316 span: field.span.clone(),
317 severity: Severity::Error,
318 message: format!("Undefined IS reference: {}", ref_name),
319 suggestion: None,
320 });
321 }
322 }
323 } else {
324 let suggestion = find_closest_match(
325 &field.name,
326 schema.fields.iter().map(|field| field.name.as_str()),
327 );
328 diagnostics.add(Diagnostic {
329 span: field.span.clone(),
330 severity: Severity::Warning,
331 message: format!(
332 "Unknown field '{}' for node '{}'",
333 field.name, type_name
334 ),
335 suggestion: suggestion.map(|s| format!("Did you mean '{}'?", s)),
336 });
337 }
338
339 if let FieldValue::Node(child_node) = &field.value {
342 diagnostics.extend(validate_ast_node(child_node, context));
343 } else if let FieldValue::Array(arr) = &field.value {
344 for el in &arr.elements {
345 if let FieldValue::Node(child_node) = &el.value {
346 diagnostics.extend(validate_ast_node(child_node, context));
347 }
348 }
349 }
350 }
351 }
352 } else {
353 diagnostics.add(Diagnostic {
354 span: node.span.clone(),
355 severity: Severity::Warning,
356 message: format!("Unknown node type '{}'", type_name),
357 suggestion: None,
358 });
359
360 for element in fields {
362 if let NodeBodyElement::Field(field) = element {
363 if let FieldValue::Node(child_node) = &field.value {
364 diagnostics.extend(validate_ast_node(child_node, context));
365 } else if let FieldValue::Array(arr) = &field.value {
366 for el in &arr.elements {
367 if let FieldValue::Node(child_node) = &el.value {
368 diagnostics.extend(validate_ast_node(child_node, context));
369 }
370 }
371 }
372 }
373 }
374 }
375 }
376 AstNodeKind::Use { use_name } => {
377 if !context.defs.contains_key(use_name) {
378 diagnostics.add(Diagnostic {
379 span: node.span.clone(),
380 severity: Severity::Error,
381 message: format!("Undefined USE reference: {}", use_name),
382 suggestion: None,
383 });
384 }
385 }
386 }
387
388 diagnostics
389}
390
391fn validate_runtime_semantics_for_node(node: &AstNode) -> DiagnosticSet {
392 let mut diagnostics = DiagnosticSet::new();
393
394 let AstNodeKind::Node {
395 type_name, fields, ..
396 } = &node.kind
397 else {
398 return diagnostics;
399 };
400
401 if matches!(type_name.as_str(), "Solid" | "Robot") {
402 if solid_requires_inertia_warning(fields) {
403 diagnostics.add(Diagnostic {
404 span: node.span.clone(),
405 severity: Severity::Warning,
406 message:
407 "Undefined inertia matrix: using the identity matrix. Please specify 'boundingObject' or 'inertiaMatrix' values."
408 .to_string(),
409 suggestion: Some(
410 "Add a non-NULL `boundingObject` on this Solid or set `Physics.inertiaMatrix`."
411 .to_string(),
412 ),
413 });
414 }
415
416 diagnostics.extend(validate_sibling_solid_name_uniqueness(fields));
417 }
418
419 diagnostics
420}
421
422fn validate_runtime_semantics_recursive(node: &AstNode, diagnostics: &mut DiagnosticSet) {
423 diagnostics.extend(validate_runtime_semantics_for_node(node));
424
425 let AstNodeKind::Node { fields, .. } = &node.kind else {
426 return;
427 };
428
429 for element in fields {
430 let NodeBodyElement::Field(field) = element else {
431 continue;
432 };
433 collect_runtime_semantics_from_value(&field.value, diagnostics);
434 }
435}
436
437fn collect_runtime_semantics_from_value(value: &FieldValue, diagnostics: &mut DiagnosticSet) {
438 match value {
439 FieldValue::Node(node) => validate_runtime_semantics_recursive(node, diagnostics),
440 FieldValue::Array(array) => {
441 for element in &array.elements {
442 collect_runtime_semantics_from_value(&element.value, diagnostics);
443 }
444 }
445 _ => {}
446 }
447}
448
449fn solid_requires_inertia_warning(fields: &[NodeBodyElement]) -> bool {
450 let Some(physics_field) = get_node_field(fields, "physics") else {
451 return false;
452 };
453
454 let physics_node = match &physics_field.value {
456 FieldValue::Node(node) => node,
457 FieldValue::Null => return false,
458 FieldValue::Template(_) | FieldValue::Is(_) => return false,
459 _ => return false,
460 };
461
462 let has_bounding_object = matches!(
463 get_node_field(fields, "boundingObject").map(|f| &f.value),
464 Some(FieldValue::Node(_)) | Some(FieldValue::Template(_)) | Some(FieldValue::Is(_))
465 );
466
467 if has_bounding_object {
468 return false;
469 }
470
471 !physics_has_explicit_inertia_matrix(physics_node)
472}
473
474fn physics_has_explicit_inertia_matrix(physics_node: &AstNode) -> bool {
475 let AstNodeKind::Node { fields, .. } = &physics_node.kind else {
476 return false;
477 };
478
479 let Some(inertia_matrix) = get_node_field(fields, "inertiaMatrix") else {
480 return false;
481 };
482
483 match &inertia_matrix.value {
484 FieldValue::Null => false,
485 FieldValue::Array(items) => !items.elements.is_empty(),
486 FieldValue::Template(_) | FieldValue::Is(_) => true,
487 _ => true,
488 }
489}
490
491fn validate_sibling_solid_name_uniqueness(fields: &[NodeBodyElement]) -> DiagnosticSet {
492 let mut diagnostics = DiagnosticSet::new();
493 let mut seen: HashMap<String, Span> = HashMap::new();
494
495 let child_solids = collect_immediate_child_solids(fields);
496 for child_solid in child_solids {
497 if let Some(first_span) = seen.get(&child_solid.name) {
498 diagnostics.add(Diagnostic {
499 span: child_solid.span.clone(),
500 severity: Severity::Warning,
501 message: format!(
502 "'name' field value should be unique among sibling Solid nodes: '{}'",
503 child_solid.name
504 ),
505 suggestion: Some(format!(
506 "Rename one of the sibling Solid nodes; first occurrence is at line {}.",
507 first_span.start_line
508 )),
509 });
510 } else {
511 seen.insert(child_solid.name, child_solid.span);
512 }
513 }
514
515 diagnostics
516}
517
518#[derive(Debug)]
519struct ChildSolidInfo {
520 name: String,
521 span: Span,
522}
523
524fn collect_immediate_child_solids(fields: &[NodeBodyElement]) -> Vec<ChildSolidInfo> {
525 let Some(children_field) = get_node_field(fields, "children") else {
526 return Vec::new();
527 };
528
529 collect_child_solids_from_value(&children_field.value)
530}
531
532fn collect_child_solids_from_value(value: &FieldValue) -> Vec<ChildSolidInfo> {
533 match value {
534 FieldValue::Node(node) => collect_child_solids_from_node(node),
535 FieldValue::Array(array) => {
536 let mut solids = Vec::new();
537 for element in &array.elements {
538 solids.extend(collect_child_solids_from_value(&element.value));
539 }
540 solids
541 }
542 _ => Vec::new(),
543 }
544}
545
546fn collect_child_solids_from_node(node: &AstNode) -> Vec<ChildSolidInfo> {
547 let AstNodeKind::Node {
548 type_name, fields, ..
549 } = &node.kind
550 else {
551 return Vec::new();
552 };
553
554 if type_name == "Solid" {
555 return vec![ChildSolidInfo {
556 name: get_effective_solid_name(node),
557 span: node.span.clone(),
558 }];
559 }
560
561 if let Some(end_point) = get_node_field(fields, "endPoint")
563 && let FieldValue::Node(end_point_node) = &end_point.value
564 && is_solid_node(end_point_node)
565 {
566 return vec![ChildSolidInfo {
567 name: get_effective_solid_name(end_point_node),
568 span: end_point_node.span.clone(),
569 }];
570 }
571
572 Vec::new()
573}
574
575fn is_solid_node(node: &AstNode) -> bool {
576 matches!(
577 &node.kind,
578 AstNodeKind::Node {
579 type_name,
580 fields: _,
581 def_name: _,
582 } if type_name == "Solid"
583 )
584}
585
586fn get_effective_solid_name(node: &AstNode) -> String {
587 let AstNodeKind::Node { fields, .. } = &node.kind else {
588 return "solid".to_string();
589 };
590
591 if let Some(name_field) = get_node_field(fields, "name")
592 && let FieldValue::String(name) = &name_field.value
593 {
594 return name.clone();
595 }
596
597 "solid".to_string()
598}
599
600fn get_node_field<'a>(fields: &'a [NodeBodyElement], name: &str) -> Option<&'a NodeField> {
601 fields.iter().find_map(|element| match element {
602 NodeBodyElement::Field(field) if field.name == name => Some(field),
603 _ => None,
604 })
605}
606
607fn validate_node_field_semantics(
608 parent_type_name: &str,
609 field_name: &str,
610 value: &FieldValue,
611 span: &Span,
612) -> DiagnosticSet {
613 let mut diagnostics = DiagnosticSet::new();
614
615 if parent_type_name == "Shape"
616 && field_name == "geometry"
617 && let FieldValue::Node(node) = value
618 && let AstNodeKind::Node { type_name, .. } = &node.kind
619 && type_name == "CadShape"
620 {
621 diagnostics.add(Diagnostic {
622 span: span.clone(),
623 severity: Severity::Warning,
624 message: "Skipped node: Cannot insert CadShape node in 'geometry' field of Shape node."
625 .to_string(),
626 suggestion: Some(
627 "Use a geometry node supported by Shape.geometry, such as Mesh, Box, Sphere, or IndexedFaceSet."
628 .to_string(),
629 ),
630 });
631 }
632
633 diagnostics
634}
635
636fn validate_field_value(
638 value: &FieldValue,
639 expected_type: &FieldType,
640 span: &Span,
641) -> DiagnosticSet {
642 let mut diagnostics = DiagnosticSet::new();
643
644 let scalar_type_match = matches!(
645 (value, expected_type),
646 (FieldValue::Bool(_), FieldType::SFBool)
647 | (FieldValue::Int(_, _), FieldType::SFInt32)
648 | (FieldValue::Float(_, _), FieldType::SFFloat)
649 | (FieldValue::Int(_, _), FieldType::SFFloat)
650 | (FieldValue::String(_), FieldType::SFString)
651 | (FieldValue::Vec2f(_), FieldType::SFVec2f)
652 | (FieldValue::Vec3f(_), FieldType::SFVec3f)
653 | (FieldValue::Rotation(_), FieldType::SFRotation)
654 | (FieldValue::Color(_), FieldType::SFColor)
655 | (FieldValue::Node(_), FieldType::SFNode)
656 | (FieldValue::Null, FieldType::SFNode)
657 );
658
659 if scalar_type_match {
660 } else if let FieldValue::Array(arr) = value {
662 if expected_type_is_multiple(expected_type) {
663 if !validate_flat_numeric_array(arr, expected_type, span, &mut diagnostics) {
664 let element_type = get_element_type(expected_type);
665 for el in &arr.elements {
666 diagnostics.extend(validate_field_value(&el.value, &element_type, span));
667 }
668 }
669 } else {
670 diagnostics.add(Diagnostic {
671 span: span.clone(),
672 severity: Severity::Error,
673 message: format!(
674 "Type mismatch: expected {}, found {:?}",
675 field_type_name(expected_type),
676 value
677 ),
678 suggestion: None,
679 });
680 }
681 } else if matches!(value, FieldValue::Is(_)) {
682 } else if let FieldValue::NumberSequence(seq) = value {
684 if seq.elements.len() == 1 {
685 diagnostics.extend(validate_field_value(
687 &seq.elements[0].value,
688 expected_type,
689 span,
690 ));
691 } else {
692 let length = seq.elements.len();
694 if (expected_type == &FieldType::SFVec2f && length == 2)
695 || (expected_type == &FieldType::SFVec3f && length == 3)
696 || (expected_type == &FieldType::SFColor && length == 3)
697 || (expected_type == &FieldType::SFRotation && length == 4)
698 {
699 } else {
700 diagnostics.add(Diagnostic {
701 span: span.clone(),
702 severity: Severity::Error,
703 message: format!(
704 "Invalid number sequence length for {}",
705 field_type_name(expected_type)
706 ),
707 suggestion: None,
708 });
709 }
710 }
711 } else if matches!(value, FieldValue::Null) && expected_type == &FieldType::MFNode {
712 diagnostics.add(Diagnostic {
714 span: span.clone(),
715 severity: Severity::Error,
716 message: "NULL not allowed for MFNode; use [] instead".to_string(),
717 suggestion: Some("[]".to_string()),
718 });
719 } else if matches!(value, FieldValue::Template(_)) {
720 } else {
722 diagnostics.add(Diagnostic {
723 span: span.clone(),
724 severity: Severity::Error,
725 message: format!(
726 "Type mismatch: expected {}, found {:?}",
727 field_type_name(expected_type),
728 value
729 ),
730 suggestion: None,
731 });
732 }
733
734 diagnostics
735}
736
737fn validate_flat_numeric_array(
738 array: &ArrayValue,
739 expected_type: &FieldType,
740 span: &Span,
741 diagnostics: &mut DiagnosticSet,
742) -> bool {
743 let Some(group_size) = flat_numeric_group_size(expected_type) else {
744 return false;
745 };
746
747 if array.elements.is_empty() {
748 return true;
749 }
750
751 if !array
752 .elements
753 .iter()
754 .all(|element| is_numeric_scalar(&element.value))
755 {
756 return false;
757 }
758
759 if array.elements.len() % group_size != 0 {
760 diagnostics.add(Diagnostic {
761 span: span.clone(),
762 severity: Severity::Error,
763 message: format!(
764 "Invalid flat array length for {}: expected a multiple of {} values",
765 field_type_name(expected_type),
766 group_size
767 ),
768 suggestion: None,
769 });
770 }
771
772 true
773}
774
775fn flat_numeric_group_size(expected_type: &FieldType) -> Option<usize> {
776 match expected_type {
777 FieldType::MFVec2f => Some(2),
778 FieldType::MFVec3f => Some(3),
779 FieldType::MFRotation => Some(4),
780 FieldType::MFColor => Some(3),
781 _ => None,
782 }
783}
784
785fn is_numeric_scalar(value: &FieldValue) -> bool {
786 matches!(value, FieldValue::Int(_, _) | FieldValue::Float(_, _))
787}
788
789fn types_are_compatible(actual: &FieldType, expected: &FieldType) -> bool {
790 actual == expected
792}
793
794fn expected_type_is_multiple(t: &FieldType) -> bool {
795 matches!(
796 t,
797 FieldType::MFBool
798 | FieldType::MFInt32
799 | FieldType::MFFloat
800 | FieldType::MFString
801 | FieldType::MFVec2f
802 | FieldType::MFVec3f
803 | FieldType::MFColor
804 | FieldType::MFRotation
805 | FieldType::MFNode
806 )
807}
808
809fn get_element_type(t: &FieldType) -> FieldType {
810 if *t == FieldType::MFBool {
811 FieldType::SFBool
812 } else if *t == FieldType::MFInt32 {
813 FieldType::SFInt32
814 } else if *t == FieldType::MFFloat {
815 FieldType::SFFloat
816 } else if *t == FieldType::MFString {
817 FieldType::SFString
818 } else if *t == FieldType::MFVec2f {
819 FieldType::SFVec2f
820 } else if *t == FieldType::MFVec3f {
821 FieldType::SFVec3f
822 } else if *t == FieldType::MFColor {
823 FieldType::SFColor
824 } else if *t == FieldType::MFRotation {
825 FieldType::SFRotation
826 } else if *t == FieldType::MFNode {
827 FieldType::SFNode
828 } else {
829 t.clone()
830 }
831}
832
833fn field_type_name(t: &FieldType) -> String {
834 match t {
835 FieldType::SFBool => "SFBool".to_string(),
836 FieldType::SFInt32 => "SFInt32".to_string(),
837 FieldType::SFFloat => "SFFloat".to_string(),
838 FieldType::SFString => "SFString".to_string(),
839 FieldType::SFVec2f => "SFVec2f".to_string(),
840 FieldType::SFVec3f => "SFVec3f".to_string(),
841 FieldType::SFRotation => "SFRotation".to_string(),
842 FieldType::SFColor => "SFColor".to_string(),
843 FieldType::SFNode => "SFNode".to_string(),
844 FieldType::MFBool => "MFBool".to_string(),
845 FieldType::MFInt32 => "MFInt32".to_string(),
846 FieldType::MFFloat => "MFFloat".to_string(),
847 FieldType::MFString => "MFString".to_string(),
848 FieldType::MFVec2f => "MFVec2f".to_string(),
849 FieldType::MFVec3f => "MFVec3f".to_string(),
850 FieldType::MFRotation => "MFRotation".to_string(),
851 FieldType::MFColor => "MFColor".to_string(),
852 FieldType::MFNode => "MFNode".to_string(),
853 FieldType::Unknown(s) => format!("Unknown({})", s),
854 }
855}
856
857fn find_closest_match<'a>(
858 name: &str,
859 candidates: impl Iterator<Item = &'a str>,
860) -> Option<&'a str> {
861 let mut best_match = None;
862 let mut best_distance = usize::MAX;
863
864 for candidate in candidates {
865 let distance = levenshtein_distance(name, candidate);
866 if distance < best_distance && distance <= 3 {
867 best_distance = distance;
868 best_match = Some(candidate);
869 }
870 }
871
872 best_match
873}
874
875fn levenshtein_distance(s1: &str, s2: &str) -> usize {
876 let v1: Vec<char> = s1.chars().collect();
877 let v2: Vec<char> = s2.chars().collect();
878 let n = v1.len();
879 let m = v2.len();
880
881 let mut dp = vec![vec![0; m + 1]; n + 1];
882
883 for (i, row) in dp.iter_mut().enumerate().take(n + 1) {
884 row[0] = i;
885 }
886 for (j, cell) in dp[0].iter_mut().enumerate().take(m + 1) {
887 *cell = j;
888 }
889
890 for i in 1..=n {
891 for j in 1..=m {
892 let cost = if v1[i - 1] == v2[j - 1] { 0 } else { 1 };
893 dp[i][j] = std::cmp::min(
894 dp[i - 1][j] + 1,
895 std::cmp::min(dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost),
896 );
897 }
898 }
899
900 dp[n][m]
901}
902
903impl IntoIterator for DiagnosticSet {
904 type Item = Diagnostic;
905 type IntoIter = std::vec::IntoIter<Diagnostic>;
906
907 fn into_iter(self) -> Self::IntoIter {
908 self.diagnostics.into_iter()
909 }
910}