Repository: noir-lang/noir (Rust)

Issue:
Renaming a trait when importing it prevents usage of method syntax.
I've opened a PR here with tests for the functionality I would expect: https://github.com/noir-lang/noir/pull/7631

Currently if there's a collision between a trait name and the name of a type which is currently in scope, there's no way to import this trait such that I can make use of the method syntax to call trait methods. Instead I have to use the function syntax for this trait which is a worse UX.

I would expect that I should be able to rename the trait during import to a non-colliding name and use it as normal.


Discussion:

Patch:
diff --git a/compiler/noirc_frontend/src/elaborator/path_resolution.rs b/compiler/noirc_frontend/src/elaborator/path_resolution.rs
index c0dfa90d36c..289bd44d305 100644
--- a/compiler/noirc_frontend/src/elaborator/path_resolution.rs
+++ b/compiler/noirc_frontend/src/elaborator/path_resolution.rs
@@ -94,12 +94,12 @@ enum MethodLookupResult {
     /// Found a method.
     FoundMethod(PerNs),
     /// Found a trait method and it's currently in scope.
-    FoundTraitMethod(PerNs, TraitId),
+    FoundTraitMethod(PerNs, Ident),
     /// There's only one trait method that matches, but it's not in scope
     /// (we'll warn about this to avoid introducing a large breaking change)
     FoundOneTraitMethodButNotInScope(PerNs, TraitId),
     /// Multiple trait method matches were found and they are all in scope.
-    FoundMultipleTraitMethods(Vec<TraitId>),
+    FoundMultipleTraitMethods(Vec<(TraitId, Ident)>),
 }
 
 impl Elaborator<'_> {
@@ -281,9 +281,8 @@ impl Elaborator<'_> {
                         }
                     }
                     MethodLookupResult::FoundMethod(per_ns) => per_ns,
-                    MethodLookupResult::FoundTraitMethod(per_ns, trait_id) => {
-                        let trait_ = self.interner.get_trait(trait_id);
-                        self.usage_tracker.mark_as_used(importing_module, &trait_.name);
+                    MethodLookupResult::FoundTraitMethod(per_ns, name) => {
+                        self.usage_tracker.mark_as_used(importing_module, &name);
                         per_ns
                     }
                     MethodLookupResult::FoundOneTraitMethodButNotInScope(per_ns, trait_id) => {
@@ -296,9 +295,9 @@ impl Elaborator<'_> {
                         per_ns
                     }
                     MethodLookupResult::FoundMultipleTraitMethods(vec) => {
-                        let traits = vecmap(vec, |trait_id| {
+                        let traits = vecmap(vec, |(trait_id, name)| {
                             let trait_ = self.interner.get_trait(trait_id);
-                            self.usage_tracker.mark_as_used(importing_module, &trait_.name);
+                            self.usage_tracker.mark_as_used(importing_module, &name);
                             self.fully_qualified_trait_path(trait_)
                         });
                         return Err(PathResolutionError::MultipleTraitsInScope {
@@ -380,16 +379,9 @@ impl Elaborator<'_> {
 
         for (trait_id, item) in values.iter() {
             let trait_id = trait_id.expect("The None option was already considered before");
-            let trait_ = self.interner.get_trait(trait_id);
-            let Some(map) = starting_module.scope().types().get(&trait_.name) else {
-                continue;
-            };
-            let Some(imported_item) = map.get(&None) else {
-                continue;
+            if let Some(name) = starting_module.find_trait_in_scope(trait_id) {
+                results.push((trait_id, name, item));
             };
-            if imported_item.0 == ModuleDefId::TraitId(trait_id) {
-                results.push((trait_id, item));
-            }
         }
 
         if results.is_empty() {
@@ -408,13 +400,13 @@ impl Elaborator<'_> {
         }
 
         if results.len() > 1 {
-            let trait_ids = vecmap(results, |(trait_id, _)| trait_id);
+            let trait_ids = vecmap(results, |(trait_id, name, _)| (trait_id, name.clone()));
             return MethodLookupResult::FoundMultipleTraitMethods(trait_ids);
         }
 
-        let (trait_id, item) = results.remove(0);
+        let (_, name, item) = results.remove(0);
         let per_ns = PerNs { types: None, values: Some(*item) };
-        MethodLookupResult::FoundTraitMethod(per_ns, trait_id)
+        MethodLookupResult::FoundTraitMethod(per_ns, name.clone())
     }
 }
 
diff --git a/compiler/noirc_frontend/src/elaborator/types.rs b/compiler/noirc_frontend/src/elaborator/types.rs
index 6f6423017a4..7de547a1b1f 100644
--- a/compiler/noirc_frontend/src/elaborator/types.rs
+++ b/compiler/noirc_frontend/src/elaborator/types.rs
@@ -15,7 +15,7 @@ use crate::{
     elaborator::UnstableFeature,
     hir::{
         def_collector::dc_crate::CompilationError,
-        def_map::{ModuleDefId, fully_qualified_module_path},
+        def_map::fully_qualified_module_path,
         resolution::{errors::ResolverError, import::PathResolutionError},
         type_check::{
             NoMatchingImplFoundError, Source, TypeCheckError,
@@ -1570,15 +1570,7 @@ impl Elaborator<'_> {
         let traits_in_scope: Vec<_> = traits
             .iter()
             .filter_map(|trait_id| {
-                let trait_ = self.interner.get_trait(*trait_id);
-                let trait_name = &trait_.name;
-                let map = module_data.scope().types().get(trait_name)?;
-                let imported_item = map.get(&None)?;
-                if imported_item.0 == ModuleDefId::TraitId(*trait_id) {
-                    Some((*trait_id, trait_name))
-                } else {
-                    None
-                }
+                module_data.find_trait_in_scope(*trait_id).map(|name| (*trait_id, name.clone()))
             })
             .collect();
 
diff --git a/compiler/noirc_frontend/src/hir/def_map/module_data.rs b/compiler/noirc_frontend/src/hir/def_map/module_data.rs
index 5df0e08cbb2..59200305607 100644
--- a/compiler/noirc_frontend/src/hir/def_map/module_data.rs
+++ b/compiler/noirc_frontend/src/hir/def_map/module_data.rs
@@ -26,6 +26,11 @@ pub struct ModuleData {
     /// Contains only the definitions directly defined in the current module
     definitions: ItemScope,
 
+    /// All traits in scope, either from `use` imports or `trait` declarations.
+    /// The Ident value is the trait name or the `use` alias, if any.
+    /// This is stored separately from `scope` to quickly check if a trait is in scope.
+    traits_in_scope: HashMap<TraitId, Ident>,
+
     pub location: Location,
 
     /// True if this module is a `contract Foo { ... }` module containing contract functions
@@ -55,6 +60,7 @@ impl ModuleData {
             child_declaration_order: Vec::new(),
             scope: ItemScope::default(),
             definitions: ItemScope::default(),
+            traits_in_scope: HashMap::new(),
             location,
             is_contract,
             is_type,
@@ -144,6 +150,8 @@ impl ModuleData {
         visibility: ItemVisibility,
         id: TraitId,
     ) -> Result<(), (Ident, Ident)> {
+        self.traits_in_scope.insert(id, name.clone());
+
         self.declare(name, visibility, ModuleDefId::TraitId(id), None)
     }
 
@@ -167,6 +175,10 @@ impl ModuleData {
         id: ModuleDefId,
         is_prelude: bool,
     ) -> Result<(), (Ident, Ident)> {
+        if let ModuleDefId::TraitId(trait_id) = id {
+            self.traits_in_scope.insert(trait_id, name.clone());
+        }
+
         self.scope.add_item_to_namespace(name, visibility, id, None, is_prelude)
     }
 
@@ -174,6 +186,12 @@ impl ModuleData {
         self.scope.find_name(name)
     }
 
+    /// Finds a trait in scope and returns its name
+    /// (either the trait name, or a `use` alias if it was brought to scope like that)
+    pub fn find_trait_in_scope(&self, trait_id: TraitId) -> Option<&Ident> {
+        self.traits_in_scope.get(&trait_id)
+    }
+
     pub fn type_definitions(&self) -> impl Iterator<Item = ModuleDefId> + '_ {
         self.definitions.types().values().flat_map(|a| a.values().map(|(id, _, _)| *id))
     }
diff --git a/tooling/lsp/src/trait_impl_method_stub_generator.rs b/tooling/lsp/src/trait_impl_method_stub_generator.rs
index b24f4dd7d87..b828199fa88 100644
--- a/tooling/lsp/src/trait_impl_method_stub_generator.rs
+++ b/tooling/lsp/src/trait_impl_method_stub_generator.rs
@@ -255,17 +255,13 @@ impl<'a> TraitImplMethodStubGenerator<'a> {
             Type::TraitAsType(trait_id, _, trait_generics) => {
                 let trait_ = self.interner.get_trait(*trait_id);
 
+                // Check if the trait type is already imported/visible in this module
                 let current_module_data =
                     &self.def_maps[&self.module_id.krate].modules()[self.module_id.local_id.0];
-
-                // Check if the trait type is already imported/visible in this module
-                let per_ns = current_module_data.find_name(&trait_.name);
-                if let Some((module_def_id, _, _)) = per_ns.types {
-                    if module_def_id == ModuleDefId::TraitId(*trait_id) {
-                        self.string.push_str(&trait_.name.0.contents);
-                        self.append_trait_generics(trait_generics);
-                        return;
-                    }
+                if current_module_data.find_trait_in_scope(*trait_id).is_some() {
+                    self.string.push_str(&trait_.name.0.contents);
+                    self.append_trait_generics(trait_generics);
+                    return;
                 }
 
                 let parent_module_id =


Test patch:
diff --git a/compiler/noirc_frontend/src/tests/traits.rs b/compiler/noirc_frontend/src/tests/traits.rs
index 51056a1c749..6b1996a8cec 100644
--- a/compiler/noirc_frontend/src/tests/traits.rs
+++ b/compiler/noirc_frontend/src/tests/traits.rs
@@ -1246,9 +1246,7 @@ fn as_trait_path_in_expression() {
     assert_no_errors(src);
 }
 
-// TODO: remove `should_panic` once fixed
 #[test]
-#[should_panic = "Expected no errors"]
 fn allows_renaming_trait_during_import() {
     // Regression test for https://github.com/noir-lang/noir/issues/7632
     let src = r#"
@@ -1269,9 +1267,7 @@ fn allows_renaming_trait_during_import() {
     assert_no_errors(src);
 }
 
-// TODO: remove `should_panic` once fixed
 #[test]
-#[should_panic = "Expected no errors"]
 fn renaming_trait_avoids_name_collisions() {
     // Regression test for https://github.com/noir-lang/noir/issues/7632
     let src = r#"