From f6ab61c80dfafd7fabb95aa88af6be14d1fc6767 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Thu, 26 Feb 2026 16:05:53 +0000 Subject: [PATCH] fix(module): stabilize fuzzy require resolution across duplicate suffix matches When multiple indexed modules share the same trailing require path (for example plugin/treesitter-context.lua and lua/treesitter-context.lua), fuzzy lookup previously returned the first inserted candidate. Because indexing/insertion order can vary, require('treesitter-context') could intermittently resolve to different files and sometimes degrade to any when a non-exporting candidate won. This change makes fuzzy resolution deterministic by selecting the best match using: - smallest leading-segment count before the requested module path - lexicographic full-module-name tie-break for stable ordering This preserves existing fuzzy behavior while removing insertion-order flakiness. --- .../src/db_index/module/mod.rs | 45 +++++++++++++------ .../src/db_index/module/test.rs | 23 ++++++++++ 2 files changed, 54 insertions(+), 14 deletions(-) diff --git a/crates/emmylua_code_analysis/src/db_index/module/mod.rs b/crates/emmylua_code_analysis/src/db_index/module/mod.rs index 5b2b9773d..5f21b6661 100644 --- a/crates/emmylua_code_analysis/src/db_index/module/mod.rs +++ b/crates/emmylua_code_analysis/src/db_index/module/mod.rs @@ -254,23 +254,40 @@ impl LuaModuleIndex { self.file_module_map.get(file_id) } + /// Find a module by suffix when exact lookup fails. + /// + /// Candidates must either exactly equal `module_path` or end with `.{module_path}`. + /// Among matches, prefer the one with the fewest leading path segments before the suffix, + /// then use lexicographic `full_module_name` ordering as a stable tie-break. fn fuzzy_find_module(&self, module_path: &str, last_name: &str) -> Option<&ModuleInfo> { let file_ids = self.module_name_to_file_ids.get(last_name)?; let suffix_with_boundary = format!(".{}", module_path); - - // find the first matched module - for file_id in file_ids { - let module_info = self.file_module_map.get(file_id)?; - if module_info.full_module_name == module_path - || module_info - .full_module_name - .ends_with(&suffix_with_boundary) - { - return Some(module_info); - } - } - - None + file_ids + .iter() + .filter_map(|file_id| { + let module_info = self.file_module_map.get(file_id)?; + let full_module_name = module_info.full_module_name.as_str(); + let leading_segment_count = if full_module_name == module_path { + Some(0) + } else { + full_module_name + .strip_suffix(&suffix_with_boundary) + .map(|prefix| { + prefix + .split('.') + .filter(|segment| !segment.is_empty()) + .count() + }) + }?; + + Some((leading_segment_count, module_info)) + }) + .min_by(|(left_count, left_info), (right_count, right_info)| { + left_count + .cmp(right_count) + .then_with(|| left_info.full_module_name.cmp(&right_info.full_module_name)) + }) + .map(|(_, module_info)| module_info) } /// Find a module node by module path. diff --git a/crates/emmylua_code_analysis/src/db_index/module/test.rs b/crates/emmylua_code_analysis/src/db_index/module/test.rs index 490216ff9..0fc2d535d 100644 --- a/crates/emmylua_code_analysis/src/db_index/module/test.rs +++ b/crates/emmylua_code_analysis/src/db_index/module/test.rs @@ -187,4 +187,27 @@ mod tests { let module_info = m.find_module("event").unwrap(); assert_eq!(module_info.full_module_name, "nvim-cmp.lua.cmp.utils.event"); } + + #[test] + fn test_require_fuzzy_match_prefers_shortest_prefix_independent_of_insert_order() { + const PLUGIN_ENTRY: &str = "C:/Users/username/Documents/plugin/treesitter-context.lua"; + const LUA_ENTRY: &str = "C:/Users/username/Documents/lua/treesitter-context.lua"; + + // Validate both insertion orders to ensure lookup does not depend on indexing order. + for paths in [[PLUGIN_ENTRY, LUA_ENTRY], [LUA_ENTRY, PLUGIN_ENTRY]] { + let mut m = LuaModuleIndex::new(); + m.update_config(Arc::new(Emmyrc::default())); + m.add_workspace_root( + Path::new("C:/Users/username/Documents").into(), + WorkspaceId::MAIN, + ); + + for (file_id, path) in [FileId { id: 1 }, FileId { id: 2 }].into_iter().zip(paths) { + m.add_module_by_path(file_id, path); + } + + let module_info = m.find_module("treesitter-context").unwrap(); + assert_eq!(module_info.full_module_name, "lua.treesitter-context"); + } + } }