From a21e15f31a490e5d9d9dddd91fec8b9bd1d44786 Mon Sep 17 00:00:00 2001 From: sheng Date: Tue, 11 Aug 2026 10:16:17 +0800 Subject: [PATCH] Add fuzzy-search-based error suggestions to validators --- MODULE.bazel | 4 + validation/core/BUILD | 1 + .../negative_extra_unit/expected.yaml | 1 + .../negative_missing_unit/expected.yaml | 1 + .../component_internal_api/BUILD | 1 + .../component_internal_api_suite.rs | 5 ++ .../BUILD | 38 +++++++++ .../component_diagram.puml | 27 ++++++ .../expected.yaml | 20 +++++ .../internal_api_diagram.puml | 22 +++++ .../expected.yaml | 1 + .../negative_mixed_mismatch/expected.yaml | 2 + .../negative_orphan_participant/expected.yaml | 1 + validation/core/src/results/error_models.rs | 11 +++ .../validators/bazel_component_validator.rs | 43 +++++++++- .../class_design_implementation_validator.rs | 84 +++++++++++++++---- .../component_internal_api_validator.rs | 11 ++- .../component_public_api_validator.rs | 13 ++- .../component_sequence_validator.rs | 77 ++++++++++------- .../sequence_internal_api_validator.rs | 62 +++++++++++++- .../core/src/validators/shared/helpers.rs | 26 ++++++ validation/core/src/validators/shared/mod.rs | 3 +- 22 files changed, 401 insertions(+), 53 deletions(-) create mode 100644 validation/core/integration_test/component_internal_api/negative_interface_missing_from_internal_api_with_suggestion/BUILD create mode 100644 validation/core/integration_test/component_internal_api/negative_interface_missing_from_internal_api_with_suggestion/component_diagram.puml create mode 100644 validation/core/integration_test/component_internal_api/negative_interface_missing_from_internal_api_with_suggestion/expected.yaml create mode 100644 validation/core/integration_test/component_internal_api/negative_interface_missing_from_internal_api_with_suggestion/internal_api_diagram.puml diff --git a/MODULE.bazel b/MODULE.bazel index 6121d59e..a5c60cb7 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -145,6 +145,10 @@ crate.spec( package = "serde_yaml", version = "0.9", ) +crate.spec( + package = "strsim", + version = "0.11", +) crate.spec( package = "once_cell", version = "1.19", diff --git a/validation/core/BUILD b/validation/core/BUILD index 78ee2a3c..2edf1253 100644 --- a/validation/core/BUILD +++ b/validation/core/BUILD @@ -87,6 +87,7 @@ rust_library( "@crates//:log", "@crates//:serde", "@crates//:serde_json", + "@crates//:strsim", ], ) diff --git a/validation/core/integration_test/bazel_component/negative_extra_unit/expected.yaml b/validation/core/integration_test/bazel_component/negative_extra_unit/expected.yaml index 2cdc25f6..265ef546 100644 --- a/validation/core/integration_test/bazel_component/negative_extra_unit/expected.yaml +++ b/validation/core/integration_test/bazel_component/negative_extra_unit/expected.yaml @@ -17,4 +17,5 @@ error_contains: | Parent : component_example Component source file : "validation/core/integration_test/bazel_component/negative_extra_unit/component_diagram.puml" Component source line : 20 + Suggestion : did you mean unit "unit_1"? Fix : Add the corresponding Bazel unit definition for "unit_3", or remove it from the PlantUML component diagram. diff --git a/validation/core/integration_test/bazel_component/negative_missing_unit/expected.yaml b/validation/core/integration_test/bazel_component/negative_missing_unit/expected.yaml index a46e3019..ab8d40d5 100644 --- a/validation/core/integration_test/bazel_component/negative_missing_unit/expected.yaml +++ b/validation/core/integration_test/bazel_component/negative_missing_unit/expected.yaml @@ -17,4 +17,5 @@ error_contains: | Parent : component_example Stereotype : <> Bazel label : @//bazel/rules/rules_score/examples/seooc/unit_2:unit_2 + Suggestion : did you mean unit "unit_1"? Fix : Add unit "unit_2" with stereotype <> in the PlantUML component diagram, or remove it from Bazel. diff --git a/validation/core/integration_test/component_internal_api/BUILD b/validation/core/integration_test/component_internal_api/BUILD index f9521319..3efe620b 100644 --- a/validation/core/integration_test/component_internal_api/BUILD +++ b/validation/core/integration_test/component_internal_api/BUILD @@ -21,6 +21,7 @@ filegroup( "//validation/core/integration_test/component_internal_api/negative_duplicate_interface_alias_casefolded:case_data", "//validation/core/integration_test/component_internal_api/negative_duplicate_unit_alias_casefolded:case_data", "//validation/core/integration_test/component_internal_api/negative_interface_missing_from_internal_api:case_data", + "//validation/core/integration_test/component_internal_api/negative_interface_missing_from_internal_api_with_suggestion:case_data", "//validation/core/integration_test/component_internal_api/positive_interface_match:case_data", ], ) diff --git a/validation/core/integration_test/component_internal_api/component_internal_api_suite.rs b/validation/core/integration_test/component_internal_api/component_internal_api_suite.rs index cec6b424..5d14b6b5 100644 --- a/validation/core/integration_test/component_internal_api/component_internal_api_suite.rs +++ b/validation/core/integration_test/component_internal_api/component_internal_api_suite.rs @@ -56,6 +56,11 @@ fn negative_interface_missing_from_internal_api_suite_case() { assert_case("negative_interface_missing_from_internal_api"); } +#[test] +fn negative_interface_missing_from_internal_api_with_suggestion_suite_case() { + assert_case("negative_interface_missing_from_internal_api_with_suggestion"); +} + #[test] fn negative_duplicate_unit_alias_casefolded_suite_case() { assert_case("negative_duplicate_unit_alias_casefolded"); diff --git a/validation/core/integration_test/component_internal_api/negative_interface_missing_from_internal_api_with_suggestion/BUILD b/validation/core/integration_test/component_internal_api/negative_interface_missing_from_internal_api_with_suggestion/BUILD new file mode 100644 index 00000000..26b6054f --- /dev/null +++ b/validation/core/integration_test/component_internal_api/negative_interface_missing_from_internal_api_with_suggestion/BUILD @@ -0,0 +1,38 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("//bazel/rules/rules_score:rules_score.bzl", "architectural_design") +load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixture_bundle") + +architectural_design( + name = "design", + internal_api = ["internal_api_diagram.puml"], + maturity = "development", + static = ["component_diagram.puml"], + visibility = ["//visibility:private"], +) + +provider_fbs_fixture_bundle( + name = "fbs", + visibility = ["//visibility:public"], + deps = [":design"], +) + +filegroup( + name = "case_data", + srcs = [ + "expected.yaml", + ":fbs", + ], + visibility = ["//visibility:public"], +) diff --git a/validation/core/integration_test/component_internal_api/negative_interface_missing_from_internal_api_with_suggestion/component_diagram.puml b/validation/core/integration_test/component_internal_api/negative_interface_missing_from_internal_api_with_suggestion/component_diagram.puml new file mode 100644 index 00000000..c87543a4 --- /dev/null +++ b/validation/core/integration_test/component_internal_api/negative_interface_missing_from_internal_api_with_suggestion/component_diagram.puml @@ -0,0 +1,27 @@ +' ******************************************************************************* +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* + +@startuml component_diagram + +package "Package A" as package_a { + component "Component A" as component_a <> { + component "Unit 1" as unit_1 <> + component "Unit 2" as unit_2 <> + } + + interface "InternalInterface" as InternalInterface + unit_1 -( InternalInterface + unit_2 )- InternalInterface +} + +@enduml diff --git a/validation/core/integration_test/component_internal_api/negative_interface_missing_from_internal_api_with_suggestion/expected.yaml b/validation/core/integration_test/component_internal_api/negative_interface_missing_from_internal_api_with_suggestion/expected.yaml new file mode 100644 index 00000000..f9d5adf6 --- /dev/null +++ b/validation/core/integration_test/component_internal_api/negative_interface_missing_from_internal_api_with_suggestion/expected.yaml @@ -0,0 +1,20 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +should_pass: false +error_contains: |- + [Interface] Component interface(s) "package_a.InternalInterface" from the component diagram not found in the internal API diagram. + Missing interfaces : "package_a.InternalInterface" + Component source file for "package_a.InternalInterface" : "validation/core/integration_test/component_internal_api/negative_interface_missing_from_internal_api_with_suggestion/component_diagram.puml" + Component source line for "package_a.InternalInterface" : 22 + Suggestion : did you mean interface "package_a.internal_interface"? + Fix : Add interface declaration(s) "package_a.InternalInterface" in the internal API diagram, or remove those interface declarations from the component diagram. diff --git a/validation/core/integration_test/component_internal_api/negative_interface_missing_from_internal_api_with_suggestion/internal_api_diagram.puml b/validation/core/integration_test/component_internal_api/negative_interface_missing_from_internal_api_with_suggestion/internal_api_diagram.puml new file mode 100644 index 00000000..5a9f457d --- /dev/null +++ b/validation/core/integration_test/component_internal_api/negative_interface_missing_from_internal_api_with_suggestion/internal_api_diagram.puml @@ -0,0 +1,22 @@ +' ******************************************************************************* +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* + +@startuml + +package package_a { + interface "internal_interface" as internal_interface { + + GetData() + } +} + +@enduml diff --git a/validation/core/integration_test/component_sequence/negative_missing_participant/expected.yaml b/validation/core/integration_test/component_sequence/negative_missing_participant/expected.yaml index a068742e..672b0883 100644 --- a/validation/core/integration_test/component_sequence/negative_missing_participant/expected.yaml +++ b/validation/core/integration_test/component_sequence/negative_missing_participant/expected.yaml @@ -17,6 +17,7 @@ error_contains: Alias : "unit_3" Component source file : "validation/core/integration_test/component_sequence/negative_missing_participant/component_diagram.puml" Component source line : 20 + Suggestion : did you mean "unit_1"? Fix : Add sequence participant "unit_3" in the sequence diagram, or remove it from the component diagram. - | [Interface] Sequence-connected units "unit_1" and "unit_2" have no corresponding shared interface connection in the component diagram. diff --git a/validation/core/integration_test/component_sequence/negative_mixed_mismatch/expected.yaml b/validation/core/integration_test/component_sequence/negative_mixed_mismatch/expected.yaml index 0fe42931..a7d0a66c 100644 --- a/validation/core/integration_test/component_sequence/negative_mixed_mismatch/expected.yaml +++ b/validation/core/integration_test/component_sequence/negative_mixed_mismatch/expected.yaml @@ -17,12 +17,14 @@ error_contains: Alias : "unit_1" Component source file : "validation/core/integration_test/component_sequence/negative_mixed_mismatch/component_diagram.puml" Component source line : 18 + Suggestion : did you mean "unit_2"? Fix : Add sequence participant "unit_1" in the sequence diagram, or remove it from the component diagram. - | [Naming] Participant "unit_3" from the sequence diagram not found in the component diagram. Participant : "unit_3" Sequence source file : "validation/core/integration_test/component_sequence/negative_mixed_mismatch/sequence_diagram.puml" Sequence source line : 17 + Suggestion : did you mean "unit_1"? Fix : Add component unit alias "unit_3" in the component diagram, or remove it from the sequence diagram. - | [Interface] Sequence-connected units "unit_2" and "unit_3" have no corresponding shared interface connection in the component diagram. diff --git a/validation/core/integration_test/component_sequence/negative_orphan_participant/expected.yaml b/validation/core/integration_test/component_sequence/negative_orphan_participant/expected.yaml index d0c9ee70..71914dfd 100644 --- a/validation/core/integration_test/component_sequence/negative_orphan_participant/expected.yaml +++ b/validation/core/integration_test/component_sequence/negative_orphan_participant/expected.yaml @@ -17,6 +17,7 @@ error_contains: Participant : "unit_3" Sequence source file : "validation/core/integration_test/component_sequence/negative_orphan_participant/sequence_diagram.puml" Sequence source line : 17 + Suggestion : did you mean "unit_1"? Fix : Add component unit alias "unit_3" in the component diagram, or remove it from the sequence diagram. - | [Interface] Sequence-connected units "unit_1" and "unit_3" have no corresponding shared interface connection in the component diagram. diff --git a/validation/core/src/results/error_models.rs b/validation/core/src/results/error_models.rs index 951e64d0..83a8a1bf 100644 --- a/validation/core/src/results/error_models.rs +++ b/validation/core/src/results/error_models.rs @@ -96,6 +96,17 @@ impl ErrorBuilder { self } + pub fn suggest(self, suggestion_subject: Option<&str>, suggested_name: &str) -> Self { + let suggestion_text = match suggestion_subject { + Some(suggestion_subject) => { + format!("did you mean {suggestion_subject} \"{suggested_name}\"?") + } + None => format!("did you mean \"{suggested_name}\"?"), + }; + + self.field("suggestion", suggestion_text) + } + pub fn build(self) -> String { let label_width = self .fields diff --git a/validation/core/src/validators/bazel_component_validator.rs b/validation/core/src/validators/bazel_component_validator.rs index a44090a8..a44edf86 100644 --- a/validation/core/src/validators/bazel_component_validator.rs +++ b/validation/core/src/validators/bazel_component_validator.rs @@ -19,6 +19,7 @@ use std::collections::{BTreeMap, BTreeSet}; +use super::shared::best_string_suggestion; use crate::models::{BazelArchitecture, ComponentDiagramArchitecture, LogicComponent}; use crate::results::{ErrorBuilder, ErrorCategory}; use crate::{Diagnostics, ValidationResult}; @@ -93,12 +94,20 @@ impl BazelComponentValidator { if !diagram_set.contains_key(key) { let (name, parent) = key; let parent_str = Self::parent_display(parent, default_parent); + let suggested_alias = best_string_suggestion( + name, + diagram_set + .keys() + .filter(|(_, candidate_parent)| candidate_parent == parent) + .map(|(candidate_name, _)| candidate_name.as_str()), + ); self.result.add_failure(Self::format_missing( display_type, stereotype, name, &parent_str, label, + suggested_alias.as_deref(), )); } } @@ -108,11 +117,19 @@ impl BazelComponentValidator { if !bazel_set.contains_key(key) { let (name, parent) = key; let parent_str = Self::parent_display(parent, default_parent); + let suggested_alias = best_string_suggestion( + name, + bazel_set + .keys() + .filter(|(_, candidate_parent)| candidate_parent == parent) + .map(|(candidate_name, _)| candidate_name.as_str()), + ); self.result.add_failure(Self::format_extra( display_type, name, &parent_str, entity, + suggested_alias.as_deref(), )); } } @@ -176,15 +193,24 @@ impl BazelComponentValidator { name: &str, parent_str: &str, label: &str, + suggested_alias: Option<&str>, ) -> String { - ErrorBuilder::new(ErrorCategory::Naming) + let error = ErrorBuilder::new(ErrorCategory::Naming) .title(format!( "{display_type} \"{name}\" from Bazel not found in the PlantUML component diagram" )) .field("alias", format!("\"{name}\"")) .field("parent", parent_str) .field("stereotype", format!("<<{stereotype}>>")) - .field("bazel label", label) + .field("bazel label", label); + + let error = if let Some(suggested_alias) = suggested_alias { + error.suggest(Some(display_type), suggested_alias) + } else { + error + }; + + error .fix(format!( "add {display_type} \"{name}\" with stereotype <<{stereotype}>> in the PlantUML component diagram, or remove it from Bazel" )) @@ -196,17 +222,26 @@ impl BazelComponentValidator { name: &str, parent_str: &str, entity: &LogicComponent, + suggested_alias: Option<&str>, ) -> String { let (source_file, source_line) = entity.source_location.display(); - ErrorBuilder::new(ErrorCategory::Naming) + let error = ErrorBuilder::new(ErrorCategory::Naming) .title(format!( "{entity_type} \"{name}\" from the PlantUML component diagram not found in Bazel" )) .field("alias", format!("\"{name}\"")) .field("parent", parent_str) .field("component source file", format!("\"{source_file}\"")) - .field("component source line", source_line.to_string()) + .field("component source line", source_line.to_string()); + + let error = if let Some(suggested_alias) = suggested_alias { + error.suggest(Some(entity_type), suggested_alias) + } else { + error + }; + + error .fix(format!( "add the corresponding Bazel {entity_type} definition for \"{name}\", or remove it from the PlantUML component diagram" )) diff --git a/validation/core/src/validators/class_design_implementation_validator.rs b/validation/core/src/validators/class_design_implementation_validator.rs index db3ac953..fe89aad1 100644 --- a/validation/core/src/validators/class_design_implementation_validator.rs +++ b/validation/core/src/validators/class_design_implementation_validator.rs @@ -14,6 +14,7 @@ //! Class implementation validation: compare unit design class diagrams with //! C++ implementation produced by the C++ parser. +use super::shared::best_string_suggestion; use crate::models::ClassEntityIndex; use crate::results::{ErrorBuilder, ErrorCategory}; use crate::ValidationResult; @@ -67,8 +68,16 @@ impl ClassDesignImplementationValidator { .find_by_id(&design_entity.id) .or_else(|| implementation_classes.find_by_id(&normalized_design_id)) else { - self.result - .add_failure(Self::format_missing_class(design_entity)); + let suggested_class = best_string_suggestion( + &design_entity.id, + implementation_classes + .entities() + .map(|entity| entity.id.as_str()), + ); + self.result.add_failure(Self::format_missing_class( + design_entity, + suggested_class.as_deref(), + )); continue; }; @@ -148,6 +157,11 @@ impl ClassDesignImplementationValidator { design_entity, "type_alias", &design_alias.alias, + best_string_suggestion( + &design_alias.alias, + implementation_aliases.keys().copied(), + ) + .as_deref(), )), } } @@ -173,6 +187,11 @@ impl ClassDesignImplementationValidator { design_entity, "variable", &design_variable.name, + best_string_suggestion( + &design_variable.name, + implementation_variables.keys().copied(), + ) + .as_deref(), )), } } @@ -257,6 +276,11 @@ impl ClassDesignImplementationValidator { design_entity, "method", &key, + best_string_suggestion( + &key, + implementation_methods.keys().map(String::as_str), + ) + .as_deref(), )), } } @@ -412,6 +436,11 @@ impl ClassDesignImplementationValidator { design_entity, "enum_literal", &design_literal.name, + best_string_suggestion( + &design_literal.name, + implementation_literals.keys().copied(), + ) + .as_deref(), )), } } @@ -440,17 +469,29 @@ impl ClassDesignImplementationValidator { &relationship_display_name(implementation_relationship), )) } - None => self.result.add_failure(Self::format_missing_member( - design_entity, - "relationship", - &display_name, - )), + None => { + let implementation_relationship_names = implementation_entity + .relationships + .iter() + .map(relationship_display_name) + .collect::>(); + self.result.add_failure(Self::format_missing_member( + design_entity, + "relationship", + &display_name, + best_string_suggestion( + &display_name, + implementation_relationship_names.iter().map(String::as_str), + ) + .as_deref(), + )) + } } } } - fn format_missing_class(entity: &SimpleEntity) -> String { - ErrorBuilder::new(ErrorCategory::Class) + fn format_missing_class(entity: &SimpleEntity, suggested_class: Option<&str>) -> String { + let error = ErrorBuilder::new(ErrorCategory::Class) .title(format!( "class \"{}\" from the unit design not found in the C++ class implementation", entity.id @@ -461,16 +502,24 @@ impl ClassDesignImplementationValidator { .fix(format!( "add implementation class \"{}\" in the C++ class implementation, or remove it from the unit design", entity.id - )) - .build() + )); + + let error = if let Some(suggested_class) = suggested_class { + error.suggest(Some("class"), suggested_class) + } else { + error + }; + + error.build() } fn format_missing_member( design_entity: &SimpleEntity, member_type: &str, member_name: &str, + suggested_member: Option<&str>, ) -> String { - ErrorBuilder::new(ErrorCategory::Member) + let error = ErrorBuilder::new(ErrorCategory::Member) .title(format!( "{member_type} \"{member_name}\" from entity \"{}\" in the unit design not found in the C++ class implementation", design_entity.id @@ -485,8 +534,15 @@ impl ClassDesignImplementationValidator { .fix(format!( "add {member_type} \"{member_name}\" to entity \"{}\" in the C++ class implementation, or remove it from the unit design", design_entity.id - )) - .build() + )); + + let error = if let Some(suggested_member) = suggested_member { + error.suggest(Some(member_type), suggested_member) + } else { + error + }; + + error.build() } fn format_mismatch( diff --git a/validation/core/src/validators/component_internal_api_validator.rs b/validation/core/src/validators/component_internal_api_validator.rs index e903557d..12d47bf2 100644 --- a/validation/core/src/validators/component_internal_api_validator.rs +++ b/validation/core/src/validators/component_internal_api_validator.rs @@ -16,7 +16,7 @@ use std::collections::{BTreeMap, BTreeSet}; -use super::shared::format_name_list; +use super::shared::{best_string_suggestion, format_name_list}; use crate::models::{ComponentDiagramArchitecture, InternalApiIndex, LogicComponentExt}; use crate::results::{ErrorBuilder, ErrorCategory}; use crate::{Diagnostics, ValidationResult}; @@ -77,6 +77,7 @@ impl ComponentInternalApiValidator { .add_failure(format_missing_internal_api_interface_error( &missing_interfaces, &self.component_interface_sources, + &self.internal_api_interface_ids, )); } } @@ -119,6 +120,7 @@ fn collect_internal_api_interface_ids(internal_api_diagram: &InternalApiIndex) - fn format_missing_internal_api_interface_error( missing_internal_api_interfaces: &BTreeSet, component_interface_sources: &BTreeMap, + internal_api_interface_ids: &BTreeSet, ) -> String { let missing_interfaces = format_name_list(missing_internal_api_interfaces); @@ -141,6 +143,13 @@ fn format_missing_internal_api_interface_error( source_line.to_string(), ); } + + if let Some(suggested_interface) = best_string_suggestion( + interface_id, + internal_api_interface_ids.iter().map(String::as_str), + ) { + error = error.suggest(Some("interface"), &suggested_interface); + } } error diff --git a/validation/core/src/validators/component_public_api_validator.rs b/validation/core/src/validators/component_public_api_validator.rs index 1cb35d2c..4a837800 100644 --- a/validation/core/src/validators/component_public_api_validator.rs +++ b/validation/core/src/validators/component_public_api_validator.rs @@ -16,7 +16,7 @@ use std::collections::{BTreeMap, BTreeSet}; -use super::shared::format_name_list; +use super::shared::{best_string_suggestion, format_name_list}; use crate::models::{ComponentDiagramArchitecture, LogicComponentExt, PublicApiIndex}; use crate::results::{ErrorBuilder, ErrorCategory}; use crate::{Diagnostics, ValidationResult}; @@ -194,6 +194,17 @@ fn format_missing_public_api_error( source_line.to_string(), ); } + + if case_mismatch_public_apis.contains(interface_id) { + continue; + } + + if let Some(suggested_interface) = best_string_suggestion( + interface_id, + design_public_api_ids.iter().map(String::as_str), + ) { + error = error.suggest(Some("interface"), &suggested_interface); + } } error diff --git a/validation/core/src/validators/component_sequence_validator.rs b/validation/core/src/validators/component_sequence_validator.rs index 06febb2f..775aafcc 100644 --- a/validation/core/src/validators/component_sequence_validator.rs +++ b/validation/core/src/validators/component_sequence_validator.rs @@ -19,8 +19,8 @@ use std::collections::{BTreeMap, BTreeSet}; use sequence_logic::SourceLocation; use super::shared::{ - build_observed_call_contexts, build_unit_bindings, format_name_list, intersect_interfaces, - SequenceCallContext, UnitBindings, + best_string_suggestion, build_observed_call_contexts, build_unit_bindings, format_name_list, + intersect_interfaces, SequenceCallContext, UnitBindings, }; use crate::models::{is_external_endpoint, ComponentDiagramArchitecture, SequenceDiagramIndex}; use crate::results::{ErrorBuilder, ErrorCategory}; @@ -127,19 +127,32 @@ impl<'a> ComponentSequenceValidator<'a> { .map(|source_location| source_location.display()) .unwrap_or_default(); - self.result.add_failure( - ErrorBuilder::new(ErrorCategory::Naming) - .title(format!( - "alias \"{alias}\" from the component diagram not found in the sequence diagram" - )) - .field("alias", format!("\"{alias}\"")) - .field("component source file", format!("\"{source_file}\"")) - .field("component source line", source_line.to_string()) - .fix(format!( - "add sequence participant \"{alias}\" in the sequence diagram, or remove it from the component diagram" - )) - .build(), - ); + let error = ErrorBuilder::new(ErrorCategory::Naming) + .title(format!( + "alias \"{alias}\" from the component diagram not found in the sequence diagram" + )) + .field("alias", format!("\"{alias}\"")) + .field("component source file", format!("\"{source_file}\"")) + .field("component source line", source_line.to_string()) + .fix(format!( + "add sequence participant \"{alias}\" in the sequence diagram, or remove it from the component diagram" + )); + + let error = if let Some(suggested_name) = best_string_suggestion( + alias, + self.participants + .keys() + .filter(|participant| !is_external_endpoint(participant)) + .map(String::as_str), + ) + .as_deref() + { + error.suggest(None, suggested_name) + } else { + error + }; + + self.result.add_failure(error.build()); } for participant in self.participants.keys().filter(|participant| { @@ -147,19 +160,27 @@ impl<'a> ComponentSequenceValidator<'a> { }) { let (source_file, source_line) = self.participants[participant].display(); - self.result.add_failure( - ErrorBuilder::new(ErrorCategory::Naming) - .title(format!( - "participant \"{participant}\" from the sequence diagram not found in the component diagram" - )) - .field("participant", format!("\"{participant}\"")) - .field("sequence source file", format!("\"{source_file}\"")) - .field("sequence source line", source_line.to_string()) - .fix(format!( - "add component unit alias \"{participant}\" in the component diagram, or remove it from the sequence diagram" - )) - .build(), - ); + let error = ErrorBuilder::new(ErrorCategory::Naming) + .title(format!( + "participant \"{participant}\" from the sequence diagram not found in the component diagram" + )) + .field("participant", format!("\"{participant}\"")) + .field("sequence source file", format!("\"{source_file}\"")) + .field("sequence source line", source_line.to_string()) + .fix(format!( + "add component unit alias \"{participant}\" in the component diagram, or remove it from the sequence diagram" + )); + + let error = if let Some(suggested_name) = + best_string_suggestion(participant, self.unit_bindings.keys().map(String::as_str)) + .as_deref() + { + error.suggest(None, suggested_name) + } else { + error + }; + + self.result.add_failure(error.build()); } } diff --git a/validation/core/src/validators/sequence_internal_api_validator.rs b/validation/core/src/validators/sequence_internal_api_validator.rs index b4bb3ed0..63779e74 100644 --- a/validation/core/src/validators/sequence_internal_api_validator.rs +++ b/validation/core/src/validators/sequence_internal_api_validator.rs @@ -18,8 +18,9 @@ use std::collections::{BTreeMap, BTreeSet}; use super::shared::{ - build_observed_call_contexts, build_unit_bindings, extract_method_name, format_name_list, - format_sequence_call, intersect_interfaces, SequenceCallContext, UnitBindings, UnitInterfaces, + best_string_suggestion, build_observed_call_contexts, build_unit_bindings, extract_method_name, + format_name_list, format_sequence_call, intersect_interfaces, SequenceCallContext, + UnitBindings, UnitInterfaces, }; use crate::models::{ ComponentDiagramArchitecture, InternalApiIndex, InternalApiInterface, LogicComponentExt, @@ -156,10 +157,19 @@ impl<'a> SequenceInternalApiValidator<'a> { ); if matching_interfaces.is_empty() { + let candidate_method_names = collect_method_candidates_for_interfaces( + &self.internal_api_interfaces_by_id, + &component_context.all_interfaces, + ); + let suggested_method = best_string_suggestion( + method_name, + candidate_method_names.iter().map(String::as_str), + ); return Some(format_sequence_method_consistency_error( call_context, method_name, "sequence function name was not found in available interface methods", + suggested_method.as_deref(), )); } @@ -200,10 +210,23 @@ impl<'a> SequenceInternalApiValidator<'a> { intersect_interfaces(&caller_matching_interfaces, &callee_matching_interfaces); if shared_method_interfaces.is_empty() { + let shared_interface_ids = collect_shared_internal_api_interface_ids( + &self.internal_api_interfaces_by_id, + call_context, + ); + let candidate_method_names = collect_method_candidates_for_interfaces( + &self.internal_api_interfaces_by_id, + &shared_interface_ids, + ); + let suggested_method = best_string_suggestion( + method_name, + candidate_method_names.iter().map(String::as_str), + ); return Some(format_sequence_method_consistency_error( call_context, method_name, "sequence function name was not found in the related interface methods", + suggested_method.as_deref(), )); } @@ -401,6 +424,30 @@ fn matching_interfaces_with_method( .collect() } +fn collect_method_candidates_for_interfaces( + internal_api_interfaces_by_id: &BTreeMap, + interface_ids: &BTreeSet, +) -> BTreeSet { + interface_ids + .iter() + .filter_map(|interface_id| internal_api_interfaces_by_id.get(interface_id.as_str())) + .flat_map(|interface| interface.method_names.iter().cloned()) + .collect() +} + +fn collect_shared_internal_api_interface_ids( + internal_api_interfaces_by_id: &BTreeMap, + call_context: &SequenceCallContext<'_>, +) -> BTreeSet { + intersect_interfaces( + &call_context.caller_interfaces, + &call_context.callee_interfaces, + ) + .into_iter() + .filter(|interface_id| internal_api_interfaces_by_id.contains_key(interface_id.as_str())) + .collect() +} + fn role_interfaces(bindings: &UnitInterfaces) -> BTreeSet { bindings .required_interfaces @@ -478,6 +525,7 @@ fn format_sequence_method_consistency_error( call_context: &SequenceCallContext<'_>, method_name: &str, description: &str, + suggested_method: Option<&str>, ) -> String { let sequence_call = format_sequence_call( call_context.caller_unit, @@ -486,14 +534,20 @@ fn format_sequence_method_consistency_error( ); let (source_file, source_line) = call_context.source_location.display(); - ErrorBuilder::new(ErrorCategory::Method) + let mut error = ErrorBuilder::new(ErrorCategory::Method) .title(format!( "sequence function \"{method_name}\" from sequence call {sequence_call} in the sequence diagram not found in the internal API diagram" )) .field("sequence call", sequence_call.clone()) .field("sequence source file", format!("\"{source_file}\"")) .field("sequence source line", source_line.to_string()) - .field("detail", description) + .field("detail", description); + + if let Some(suggested_method) = suggested_method { + error = error.suggest(Some("method"), suggested_method); + } + + error .fix(format!( "add method \"{method_name}\" in a matching internal API interface in the internal API diagram, or remove sequence function \"{method_name}\" in sequence call {sequence_call} in the sequence diagram" )) diff --git a/validation/core/src/validators/shared/helpers.rs b/validation/core/src/validators/shared/helpers.rs index 0c2ba682..cfe1a28f 100644 --- a/validation/core/src/validators/shared/helpers.rs +++ b/validation/core/src/validators/shared/helpers.rs @@ -15,6 +15,10 @@ use std::collections::BTreeSet; +use strsim::jaro_winkler; + +pub(in crate::validators) const DEFAULT_SUGGESTION_THRESHOLD: f64 = 0.75; + pub(in crate::validators) fn format_name_list(names: &BTreeSet) -> String { if names.is_empty() { return "".to_string(); @@ -48,3 +52,25 @@ pub(in crate::validators) fn intersect_interfaces( .cloned() .collect() } + +pub(in crate::validators) fn best_string_suggestion<'a>( + name: &str, + candidates: impl IntoIterator, +) -> Option { + let mut best_candidate: Option<&str> = None; + let mut best_score = 0.0; + + for candidate in candidates { + let score = jaro_winkler(name, candidate); + if score > best_score { + best_score = score; + best_candidate = Some(candidate); + } + } + + if best_score < DEFAULT_SUGGESTION_THRESHOLD { + return None; + } + + best_candidate.map(str::to_string) +} diff --git a/validation/core/src/validators/shared/mod.rs b/validation/core/src/validators/shared/mod.rs index 1eca4cad..f67af557 100644 --- a/validation/core/src/validators/shared/mod.rs +++ b/validation/core/src/validators/shared/mod.rs @@ -21,5 +21,6 @@ pub(in crate::validators) use diagram_analysis::{ UnitInterfaces, }; pub(in crate::validators) use helpers::{ - extract_method_name, format_name_list, format_sequence_call, intersect_interfaces, + best_string_suggestion, extract_method_name, format_name_list, format_sequence_call, + intersect_interfaces, };