@@ -334,7 +334,7 @@ pub(crate) fn file_to_module(db: &dyn Db, file: File) -> Option<Module<'_>> {
334334 db,
335335 file,
336336 path,
337- desperate_search_paths ( db, file) . iter ( ) . flatten ( ) ,
337+ simple_desperate_search_paths ( db, file) . iter ( ) ,
338338 )
339339 } )
340340}
@@ -449,10 +449,64 @@ fn desperate_search_paths(db: &dyn Db, importing_file: File) -> Option<Vec<Searc
449449 if search_paths. is_empty ( ) {
450450 None
451451 } else {
452- search_paths. reverse ( ) ;
453452 Some ( search_paths)
454453 }
455454}
455+
456+ /// Get the search-paths that should be used for desperate resolution of imports in this file
457+ ///
458+ /// Currently this is "the closest ancestor dir that contains a pyproject.toml", which is
459+ /// a completely arbitrary decision. We could potentially change this to return an iterator
460+ /// of every ancestor with a pyproject.toml or every ancestor.
461+ ///
462+ /// For now this works well in common cases where we have some larger workspace that contains
463+ /// one or more python projects in sub-directories, and those python projects assume that
464+ /// absolute imports resolve relative to the pyproject.toml they live under.
465+ ///
466+ /// Being so strict minimizes concerns about this going off a lot and doing random
467+ /// chaotic things. In particular, all files under a given pyproject.toml will currently
468+ /// agree on this being their desperate search-path, which is really nice.
469+ #[ salsa:: tracked( heap_size=ruff_memory_usage:: heap_size) ]
470+ fn simple_desperate_search_paths ( db : & dyn Db , importing_file : File ) -> Option < SearchPath > {
471+ let system = db. system ( ) ;
472+ let importing_path = importing_file. path ( db) . as_system_path ( ) ?;
473+
474+ // Only allow this if the importing_file is under the first-party search path
475+ let ( base_path, rel_path) =
476+ search_paths ( db, ModuleResolveMode :: StubsAllowed ) . find_map ( |search_path| {
477+ if !search_path. is_first_party ( ) {
478+ return None ;
479+ }
480+ Some ( (
481+ search_path. as_system_path ( ) ?,
482+ search_path. relativize_system_path_only ( importing_path) ?,
483+ ) )
484+ } ) ?;
485+
486+ // Read the revision on the corresponding file root to
487+ // register an explicit dependency on this directory. When
488+ // the revision gets bumped, the cache that Salsa creates
489+ // for this routine will be invalidated.
490+ //
491+ // (This is conditional because ruff uses this code too and doesn't set roots)
492+ if let Some ( root) = db. files ( ) . root ( db, base_path) {
493+ let _ = root. revision ( db) ;
494+ }
495+
496+ // Only allow searching up to the first-party path's root
497+ for rel_dir in rel_path. ancestors ( ) {
498+ let candidate_path = base_path. join ( rel_dir) ;
499+ // Any dir with a pyproject.toml or ty.toml might be a project root
500+ if system. is_file ( & candidate_path. join ( "pyproject.toml" ) )
501+ || system. is_file ( & candidate_path. join ( "ty.toml" ) )
502+ {
503+ let search_path = SearchPath :: first_party ( system, candidate_path) . ok ( ) ?;
504+ return Some ( search_path) ;
505+ }
506+ }
507+
508+ None
509+ }
456510#[ derive( Clone , Debug , PartialEq , Eq , get_size2:: GetSize ) ]
457511pub struct SearchPaths {
458512 /// Search paths that have been statically determined purely from reading
0 commit comments