Skip to content

Commit f2a1a7e

Browse files
jvansch1meta-codesync[bot]
authored andcommitted
Add module_name_from_path helper for call hierarchy
Summary: ## Problem When processing Glean-derived external references, we have a file path but no `Handle` or pre-computed module name. We need to derive a qualified Python module name (e.g., `foo.bar.baz`) from the file path to construct fully-qualified caller names for the call hierarchy. ## Solution Add `module_name_from_path` which walks up parent directories looking for `__init__.py` package markers to build the dotted module name. Handles standalone files, packages, `__init__.py` itself, nested packages, and `.pyi` stubs. ## Changes - Add `module_name_from_path` function - Add `std::path::Path` import - Add unit test covering standalone files, packages, __init__.py, nested packages, and .pyi stubs Reviewed By: yangdanny97 Differential Revision: D96473164 fbshipit-source-id: b59d01e7d57f26b81667cd7877c9c70d861afac9
1 parent d53f256 commit f2a1a7e

File tree

1 file changed

+82
-0
lines changed

1 file changed

+82
-0
lines changed

pyrefly/lib/lsp/non_wasm/call_hierarchy.rs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8+
use std::path::Path;
9+
810
use dupe::Dupe;
911
use lsp_types::CallHierarchyIncomingCall;
1012
use lsp_types::CallHierarchyItem;
@@ -187,6 +189,43 @@ fn find_enclosing_call_range(ast: &ModModule, position: TextSize) -> Option<Text
187189
None
188190
}
189191

192+
/// Derives a Python module name from a file path by walking up parent
193+
/// directories looking for `__init__.py` package markers.
194+
fn module_name_from_path(path: &Path) -> ModuleName {
195+
let stem = path
196+
.file_stem()
197+
.and_then(|s| s.to_str())
198+
.unwrap_or("__unknown__");
199+
200+
let mut parts = if stem == "__init__" {
201+
vec![]
202+
} else {
203+
vec![stem]
204+
};
205+
206+
let mut current = path.parent();
207+
while let Some(dir) = current {
208+
if dir.join("__init__.py").exists() {
209+
if let Some(name) = dir.file_name().and_then(|n| n.to_str()) {
210+
parts.push(name);
211+
current = dir.parent();
212+
} else {
213+
break;
214+
}
215+
} else {
216+
break;
217+
}
218+
}
219+
220+
parts.reverse();
221+
222+
if parts.is_empty() {
223+
ModuleName::from_str(stem)
224+
} else {
225+
ModuleName::from_string(parts.join("."))
226+
}
227+
}
228+
190229
/// Prepares a CallHierarchyItem for a function definition.
191230
///
192231
/// Creates the LSP CallHierarchyItem representation for a function,
@@ -484,4 +523,47 @@ class MyClass:
484523
let pos_method = TextSize::from(24);
485524
assert!(find_enclosing_call_range(&ast, pos_method).is_some());
486525
}
526+
527+
#[test]
528+
fn test_module_name_from_path() {
529+
use std::fs;
530+
531+
use tempfile::tempdir;
532+
533+
use super::module_name_from_path;
534+
535+
let dir = tempdir().unwrap();
536+
let root = dir.path();
537+
538+
// Standalone file: bar.py with no __init__.py → bar
539+
let bar_py = root.join("bar.py");
540+
fs::write(&bar_py, "").unwrap();
541+
assert_eq!(module_name_from_path(&bar_py).as_str(), "bar");
542+
543+
// Package: pkg/__init__.py + pkg/baz.py → pkg.baz
544+
let pkg = root.join("pkg");
545+
fs::create_dir_all(&pkg).unwrap();
546+
fs::write(pkg.join("__init__.py"), "").unwrap();
547+
let baz_py = pkg.join("baz.py");
548+
fs::write(&baz_py, "").unwrap();
549+
assert_eq!(module_name_from_path(&baz_py).as_str(), "pkg.baz");
550+
551+
// __init__.py itself → package name
552+
let init_py = pkg.join("__init__.py");
553+
assert_eq!(module_name_from_path(&init_py).as_str(), "pkg");
554+
555+
// Nested: foo/__init__.py + foo/bar/__init__.py + foo/bar/qux.py → foo.bar.qux
556+
let nested = root.join("foo").join("bar");
557+
fs::create_dir_all(&nested).unwrap();
558+
fs::write(root.join("foo").join("__init__.py"), "").unwrap();
559+
fs::write(nested.join("__init__.py"), "").unwrap();
560+
let qux_py = nested.join("qux.py");
561+
fs::write(&qux_py, "").unwrap();
562+
assert_eq!(module_name_from_path(&qux_py).as_str(), "foo.bar.qux");
563+
564+
// .pyi stub → same as .py
565+
let stub = root.join("stub.pyi");
566+
fs::write(&stub, "").unwrap();
567+
assert_eq!(module_name_from_path(&stub).as_str(), "stub");
568+
}
487569
}

0 commit comments

Comments
 (0)