Skip to main content

brokk_bifrost_python/
graph_support.rs

1//! The language half of Python's resolution logic: module lookup, the export
2//! index, the import binder, base-class resolution and the skeleton renderer,
3//! written as free functions over a source trait instead of as methods on
4//! `PythonAnalyzer`.
5//!
6//! `PythonAnalyzer` (in `brokk-bifrost-analysis`) owns the lazy cells (seven
7//! moka caches, one `OnceLock` and two `PoolSafeMemo`s) and implements
8//! [`PythonSource`] out of its own accessors, so the functions below
9//! reach back for the memoized products they need without naming the analyzer
10//! type.
11
12use brokk_bifrost_core::analyzer::capabilities::ImportAnalysisProvider;
13use brokk_bifrost_core::analyzer::model::ImportInfo;
14use brokk_bifrost_core::analyzer::prepared_syntax::{IndexedFileFacts, PreparedSyntaxTree};
15use brokk_bifrost_core::analyzer::query_token::QueryToken;
16use brokk_bifrost_core::analyzer::symbol_path::parse_symbol_path;
17use brokk_bifrost_core::analyzer::tree_walk::{WalkControl, walk_named_tree_preorder};
18use brokk_bifrost_core::analyzer::usages::model::{
19    ExportEntry, ExportIndex, ImportBinder, ImportBinding, ImportKind, ReexportStar,
20};
21use brokk_bifrost_core::analyzer::{CodeUnit, CodeUnitIndex, Language, ProjectFile};
22use brokk_bifrost_core::hash::{HashMap, HashSet};
23use std::collections::BTreeSet;
24use std::path::{Path, PathBuf};
25use std::sync::Arc;
26use tree_sitter::Node;
27
28use crate::declarations::{
29    collect_python_identifiers, module_code_unit, parse_python_tree, python_declared_import_root,
30    python_import_root, python_module_name, python_project_module_components,
31    python_repeated_root_extension_dir,
32};
33use crate::imports::{
34    PythonImportDetails, python_import_details, python_namespace_binding_module,
35    python_namespace_binding_name, resolve_exported_fqn, resolve_import_bindings,
36    resolve_python_relative_module,
37};
38use crate::syntax::{python_plain_string_literal, python_static_attribute_path};
39use crate::usage_index::PythonUsageIndex;
40
41/// The analyzer-resident products Python's language logic resolves through, on
42/// top of the two core capability traits it reads declarations and imports
43/// with. The analyzer is the only implementor and every method forwards to one
44/// of its own accessors, so the cells stay where they are and no free function
45/// can reach past this surface.
46///
47/// The usage index is deliberately absent: [`PythonUsageIndex::build`] and
48/// everything it calls take this trait, so the build cannot re-enter the memo
49/// it is filling. Code that runs once the index exists takes
50/// [`PythonUsageSource`].
51pub trait PythonSource:
52    CodeUnitIndex + ImportAnalysisProvider + crate::source_facts::PythonSourceFactProvider
53{
54    /// Path-derived module units for `module_fq`; `None` when the store could
55    /// not answer the path-symbol query at all.
56    fn path_module_fqn(&self, module_fq: &str) -> Option<Vec<CodeUnit>>;
57
58    /// [`Self::path_module_fqn`] for a whole batch, resolved in one store
59    /// transaction.
60    fn path_module_fqns_batch(&self, module_fqs: &[String]) -> Vec<Option<Vec<CodeUnit>>>;
61
62    fn definition_fqn(&self, fqn: &str) -> Vec<CodeUnit>;
63
64    /// Every workspace Python module's project-root name, for resolving an
65    /// absolute specifier written against a source root below the project root.
66    ///
67    /// Memoized for the analyzer generation by the implementor: the product is
68    /// path math over the workspace file listing, and every import whose
69    /// specifier names no indexed module asks for it.
70    fn module_spellings(&self) -> Arc<PythonModuleSpellings>;
71
72    /// Shared by handle: both products are immutable for the analyzer
73    /// generation that cached them, and callers ask for them once per receiver
74    /// type, annotation or export name, so deep-cloning the whole map out of
75    /// the cache on every hit was pure waste.
76    fn import_binder_of(&self, file: &ProjectFile) -> Arc<ImportBinder>;
77
78    fn export_index_of(&self, file: &ProjectFile) -> Arc<ExportIndex>;
79
80    /// The admitted active syntax for executable-body and use-site queries.
81    /// Captured declaration properties are read from signature metadata.
82    /// `None` is unavailable and does not authorize a declaration parser
83    /// fallback. The token proves the request cache is live.
84    fn prepared_syntax(
85        &self,
86        token: QueryToken<'_>,
87        file: &ProjectFile,
88    ) -> Option<Arc<PreparedSyntaxTree>>;
89
90    /// Every file's indexed facts, visited in the analyzer's own bulk-read
91    /// batches. `None` marks a file the index carries no record for.
92    fn visit_file_facts(
93        &self,
94        files: &[ProjectFile],
95        visit: &mut dyn FnMut(&ProjectFile, Option<&dyn IndexedFileFacts>),
96    );
97}
98
99/// [`PythonSource`] plus the built usage index. Everything reached from
100/// the export/importer walks needs it; the index build itself must not.
101pub trait PythonUsageSource: PythonSource {
102    fn usage_index(&self) -> Arc<PythonUsageIndex>;
103}
104
105pub fn extract_type_identifiers(source: &str) -> BTreeSet<String> {
106    let Some(tree) = parse_python_tree(source) else {
107        return BTreeSet::new();
108    };
109    let mut identifiers = HashSet::default();
110    collect_python_identifiers(tree.root_node(), source, &mut identifiers);
111    identifiers.into_iter().collect()
112}
113
114/// Keep only the candidates an absolute import written in `importer` can mean.
115///
116/// A Python module name is path-derived and relative to an import root, so one
117/// snapshot that vendors two distributions side by side spells the same module
118/// twice: Feathr's `registry/purview-registry/registry/models.py` and
119/// `registry/sql-registry/registry/models.py` are both `registry.models`
120/// (#3475). Python resolves an absolute import against the `sys.path` entries
121/// the importing program runs with, and the entry always on it is the root the
122/// importing file itself lives under, so that root's module is the one the
123/// import means. An enclosing root qualifies as well as the importer's own
124/// package root, which is what lets a distribution's top-level script import
125/// its own packages; when more than one qualifies, the nearest one shadows the
126/// rest the way a closer `sys.path` entry does.
127///
128/// A candidate set that no containing root owns survives whole. That leaves a
129/// genuine cross-root import resolvable and keeps the ambiguity visible to
130/// whichever caller asked for a unique identity, rather than inventing a
131/// preference the workspace does not state.
132pub fn retain_modules_for_importer<T>(
133    importer: &ProjectFile,
134    candidates: &mut Vec<T>,
135    source_of: impl Fn(&T) -> &ProjectFile,
136) {
137    retain_nearest_owning_root(importer, candidates, |candidate| {
138        let source = source_of(candidate);
139        debug_assert_eq!(
140            importer.root(),
141            source.root(),
142            "one workspace's Python files share one project root"
143        );
144        python_import_root(source)
145    });
146}
147
148/// The `sys.path`-entry shadowing rule on its own: keep the candidates whose
149/// root contains `importer`, nearest first, and leave a set no candidate root
150/// owns untouched.
151///
152/// Shared by the two ways a candidate gets a root. A candidate found by its
153/// identity has the root [`python_import_root`] named for it; a candidate found
154/// by [`PythonModuleSpellings`] has the root the specifier was written against.
155/// Both are resolved against the importing program's path the same way.
156fn retain_nearest_owning_root<T>(
157    importer: &ProjectFile,
158    candidates: &mut Vec<T>,
159    root_of: impl Fn(&T) -> PathBuf,
160) {
161    if candidates.len() < 2 {
162        return;
163    }
164    let owning_depth = |candidate: &T| {
165        let root = root_of(candidate);
166        importer
167            .rel_path()
168            .starts_with(&root)
169            .then(|| root.components().count())
170    };
171    let depths = candidates.iter().map(owning_depth).collect::<Vec<_>>();
172    let Some(nearest) = depths.iter().copied().flatten().max() else {
173        return;
174    };
175    *candidates = std::mem::take(candidates)
176        .into_iter()
177        .zip(depths)
178        .filter(|(_, depth)| *depth == Some(nearest))
179        .map(|(candidate, _)| candidate)
180        .collect();
181}
182
183/// One workspace Python module, named from the project root.
184#[derive(Clone, Debug)]
185struct ProjectRootModuleName {
186    file: ProjectFile,
187    components: Vec<String>,
188}
189
190/// A module whose project-root name *ends with* an absolute specifier, and the
191/// source root that spelling would have to be written against.
192///
193/// This is candidate evidence, not binding evidence: a tail match only says the
194/// file could be the module if its implied root were on the importing program's
195/// path. [`PythonModuleSpellings::establishes_source_root`] decides whether the
196/// workspace's own structure makes that root real, and only an established
197/// candidate ever binds.
198#[derive(Clone, Debug)]
199pub struct PythonSourceRootModule {
200    pub file: ProjectFile,
201    /// The directory the specifier is resolved against: `file`'s relative path
202    /// with the specifier's own components removed. Never empty, because the
203    /// project root is the spelling the module index already answers exactly.
204    pub source_root: PathBuf,
205}
206
207/// Every workspace Python module's project-root name, keyed by its last
208/// component.
209///
210/// A Python module name is path-derived and relative to a `sys.path` entry, so
211/// one file has one valid absolute name per ancestor directory on that path:
212/// `dependency/core/lib/common/__init__.py` is `dependency.core.lib.common`
213/// from the project root and `core.lib.common` from `dependency/`, which is the
214/// entry dayu's `PYTHONPATH` adds and the only one its sources ever write
215/// (#3506). Exactly one of those names is the module's identity, so an import
216/// written against any other root has to be matched against the *end* of the
217/// project-root name. The last component is the one part every name of a module
218/// shares, so it is the key.
219///
220/// Built from path math over the workspace file listing: no parse, no store
221/// read, and no `sys.path` guess -- a candidate only ever comes back paired
222/// with the root that would have to be on the path for it to be the answer.
223/// The same listing states which directories are packages, which is the
224/// evidence a root without a packaging declaration is established by. A root
225/// below a packaging declaration is established by the one thing the listing
226/// cannot state -- the package marker's own `__path__` extension -- so those
227/// markers, and only the markers that could state one, are read.
228#[derive(Debug, Default)]
229pub struct PythonModuleSpellings {
230    by_last_component: HashMap<String, Vec<ProjectRootModuleName>>,
231    /// Every directory of the snapshot that holds an `__init__.py`, i.e. every
232    /// package directory the workspace states.
233    package_dirs: HashSet<PathBuf>,
234    /// The directories a package marker adds to its own `__path__`, by the
235    /// package directory that states them, once the statements a later
236    /// `__path__` assignment replaces are dropped.
237    ///
238    /// `rtdetr_pose/__init__.py` appending `rtdetr_pose/rtdetr_pose` is what
239    /// makes the shorter spelling `rtdetr_pose.config` name the file the
240    /// declared root already names `rtdetr_pose.rtdetr_pose.config`, and it is
241    /// the only thing that does: the repeated directory name, and the empty
242    /// markers that make the directories packages, state nothing -- nor does
243    /// an append in a branch that never runs, one whose guard another branch
244    /// holds, or one a later `__path__ = ...` discards (#3506).
245    path_extensions: HashMap<PathBuf, HashSet<PathBuf>>,
246}
247
248impl PythonModuleSpellings {
249    /// Index the snapshot's Python files.
250    ///
251    /// `read_source` answers a file's bytes from the same snapshot the listing
252    /// came from -- the project's own read, so an overlay's unsaved marker text
253    /// is what the evidence sees -- and is called only for markers that can
254    /// state a path extension.
255    pub fn build(
256        files: impl IntoIterator<Item = ProjectFile>,
257        mut read_source: impl FnMut(&ProjectFile) -> Option<String>,
258    ) -> Self {
259        let mut by_last_component: HashMap<String, Vec<ProjectRootModuleName>> = HashMap::default();
260        let mut package_dirs: HashSet<PathBuf> = HashSet::default();
261        let mut package_markers: Vec<ProjectFile> = Vec::new();
262        for file in files {
263            if file.rel_path().file_name().and_then(|name| name.to_str()) == Some("__init__.py")
264                && let Some(directory) = file.rel_path().parent()
265            {
266                package_dirs.insert(directory.to_path_buf());
267                package_markers.push(file.clone());
268            }
269            let components = python_project_module_components(&file);
270            let Some(last) = components.last() else {
271                continue;
272            };
273            by_last_component
274                .entry(last.clone())
275                .or_default()
276                .push(ProjectRootModuleName { file, components });
277        }
278        let mut path_extensions: HashMap<PathBuf, HashSet<PathBuf>> = HashMap::default();
279        for marker in package_markers {
280            let Some(directory) = marker.rel_path().parent() else {
281                continue;
282            };
283            if !python_package_holds_a_repeat_of_itself(directory, &package_dirs) {
284                continue;
285            }
286            let Some(source) = read_source(&marker) else {
287                continue;
288            };
289            let extended = python_appended_path_dirs(&source, marker.rel_path());
290            if !extended.is_empty() {
291                path_extensions
292                    .entry(directory.to_path_buf())
293                    .or_default()
294                    .extend(extended);
295            }
296        }
297        Self {
298            by_last_component,
299            package_dirs,
300            path_extensions,
301        }
302    }
303
304    /// The modules whose project-root name ends with `module_fq`, each with the
305    /// source root that would make the shorter name the right one.
306    ///
307    /// The project-root name itself is deliberately absent: that one is the
308    /// module's identity, which the module index answers exactly, so this
309    /// reports only the additional names and only for a dotted specifier. A
310    /// single-component specifier names a top-level module, every top-level
311    /// workspace module is already named from the project root, and a bare name
312    /// that merely matches the last component of a deeper module is a
313    /// coincidence rather than a source root -- the stdlib `typing` must not
314    /// resolve to a workspace's own `seaborn._core.typing`.
315    pub fn suffix_candidates(&self, module_fq: &str) -> Vec<PythonSourceRootModule> {
316        // A leading dot makes the specifier relative to the importing file's own
317        // package, which `resolve_python_relative_module` resolves before any
318        // lookup; no `sys.path` entry takes part in it.
319        if module_fq.starts_with('.') {
320            return Vec::new();
321        }
322        let components = parse_symbol_path(Language::Python, module_fq);
323        let Some(last) = components.last().filter(|_| components.len() > 1) else {
324            return Vec::new();
325        };
326        let mut modules = Vec::new();
327        for candidate in self
328            .by_last_component
329            .get(last.as_str())
330            .into_iter()
331            .flatten()
332        {
333            // A proper suffix only, so a shorter or equal-length name cannot
334            // match: an equal one is the identity this does not answer.
335            let Some(dropped) = candidate
336                .components
337                .len()
338                .checked_sub(components.len())
339                .filter(|dropped| *dropped > 0)
340            else {
341                continue;
342            };
343            if candidate.components[dropped..] != components[..] {
344                continue;
345            }
346            // The dropped components are the leading directories of the file's
347            // own relative path, so they are the source root exactly.
348            let source_root: PathBuf = candidate
349                .file
350                .rel_path()
351                .components()
352                .take(dropped)
353                .collect();
354            debug_assert!(
355                !source_root.as_os_str().is_empty(),
356                "dropping a component of {:?} leaves a directory",
357                candidate.file.rel_path()
358            );
359            modules.push(PythonSourceRootModule {
360                file: candidate.file.clone(),
361                source_root,
362            });
363        }
364        modules
365    }
366
367    /// Whether the workspace's own structure establishes `candidate`'s implied
368    /// source root as a root the candidate's specifier can be written against.
369    ///
370    /// Two kinds of evidence count, and a candidate with neither stays only a
371    /// candidate.
372    ///
373    /// A packaging manifest that declares a root for the candidate's file
374    /// (`tool.setuptools.packages.find.where`, `setup.py`'s `package_dir`) is
375    /// the workspace stating the root itself, and it is authoritative for the
376    /// files below it: the specifier is then the file's declared module name
377    /// and nowhere else. `source/realpkg/vendor/api.py` under
378    /// `where = ["source"]` is `realpkg.vendor.api`, so `vendor.api` -- whose
379    /// implied root is `source/realpkg` -- does not name it, even though a
380    /// package chain runs from that implied root down to it. A declaration of
381    /// the project root counts the same way: `where = ["."]` is the workspace
382    /// saying its packages begin at the project root, which is not the same as
383    /// saying nothing, so `otherpkg/vendor/api.py` is `otherpkg.vendor.api` and
384    /// `vendor.api` -- whose implied root is `otherpkg`, a package the
385    /// declaration names -- does not name it either (#3506).
386    ///
387    /// A declaration leaves one shorter spelling open, the double-nested
388    /// distribution directory, because there the specifier writes the implied
389    /// root's own name again before the module's path:
390    /// `rtdetr_pose/rtdetr_pose/config.py` is `rtdetr_pose.config` from
391    /// `rtdetr_pose/`. The shape is not the evidence, because it is only a
392    /// repeated directory name: the package the specifier starts from has to
393    /// state that the repeated directory is on its search path, by adding it to
394    /// its own `__path__` in its `__init__.py`
395    /// ([`python_repeated_root_extension_dir`] names the directory that
396    /// statement has to name), and the statement has to be one the interpreter
397    /// runs and keeps: an append behind `if False:` and an append a later
398    /// `__path__ = ...` replaces are not that statement. With that statement,
399    /// and only with it, the declared root keeps admitting the spelling; the
400    /// module still has to be reached through a real package chain from the
401    /// implied root.
402    ///
403    /// Without a declaration the evidence has to be the snapshot's own package
404    /// chain: every directory from the implied root down to the candidate's own
405    /// directory must hold an `__init__.py`, so the specifier names a complete
406    /// package/module chain that starts at that root. This is what
407    /// `dependency/core/lib/common/__init__.py` has for `core.lib.common` when
408    /// `dependency/` is on `PYTHONPATH`, and what
409    /// `rtdetr_pose/rtdetr_pose/config.py` has for `rtdetr_pose.config` in a
410    /// project that declares no packages at all. A bare tail match with no
411    /// chain behind it is not proof of a root and never binds (#3506).
412    pub fn establishes_source_root(&self, candidate: &PythonSourceRootModule) -> bool {
413        if let Some(declared) = python_declared_import_root(&candidate.file) {
414            if declared == candidate.source_root {
415                return true;
416            }
417            let Some(extension) = python_repeated_root_extension_dir(
418                &candidate.file,
419                &candidate.source_root,
420                &declared,
421            ) else {
422                return false;
423            };
424            return self.package_chain_reaches_module(candidate)
425                && self
426                    .path_extensions
427                    .get(&candidate.source_root)
428                    .is_some_and(|extended| extended.contains(&extension));
429        }
430        self.package_chain_reaches_module(candidate)
431    }
432
433    /// Whether a complete `__init__.py` package chain runs from the candidate's
434    /// implied source root down to its own module.
435    ///
436    /// This is the snapshot's own evidence that the implied root is a root the
437    /// specifier can be written against: every directory from it down to the
438    /// file's directory is a package the workspace states, so the written name
439    /// runs through that package chain rather than through a coincidence of the
440    /// file's last path component.
441    fn package_chain_reaches_module(&self, candidate: &PythonSourceRootModule) -> bool {
442        debug_assert!(
443            candidate
444                .file
445                .rel_path()
446                .starts_with(&candidate.source_root),
447            "a candidate's implied root is a prefix of its own path"
448        );
449        let mut directory = candidate.file.rel_path().parent();
450        while let Some(current) = directory {
451            if !self.package_dirs.contains(current) {
452                return false;
453            }
454            if current == candidate.source_root {
455                return true;
456            }
457            directory = current.parent();
458        }
459        false
460    }
461}
462
463/// Whether `directory` is a package that holds a directory of its own name
464/// which is also a package.
465///
466/// This is the only layout a package marker has a repeated spelling to extend
467/// `__path__` for, and it is a name-only question the listing answers, so the
468/// markers whose source could state a path extension are the only ones ever
469/// read (#3506).
470fn python_package_holds_a_repeat_of_itself(
471    directory: &Path,
472    package_dirs: &HashSet<PathBuf>,
473) -> bool {
474    directory
475        .file_name()
476        .is_some_and(|name| package_dirs.contains(&directory.join(name)))
477}
478
479/// The project-relative directories the package marker `marker_rel_path` adds
480/// to its own `__path__` and keeps there for the imports that follow.
481///
482/// The pinned YOLOZU shim states
483/// `__path__.append(os.path.join(os.path.dirname(__file__), "rtdetr_pose"))`,
484/// behind `if _impl not in __path__:`, and that executed statement -- not the
485/// nested directory's repeated name -- is what makes `rtdetr_pose.config` name
486/// `rtdetr_pose/rtdetr_pose/config.py`. Every part is recovered from AST
487/// fields: a name is the assignment that bound it, `__file__` is the marker's
488/// own path, `os.path.dirname` takes the parent and `os.path.join` joins. A
489/// statement a later `__path__ = ...` replaces contributes nothing, and a
490/// statement no shape shows the interpreter runs contributes nothing either,
491/// so an unfamiliar spelling of the same idea is a missing positive rather
492/// than a wrong one.
493fn python_appended_path_dirs(source: &str, marker_rel_path: &Path) -> Vec<PathBuf> {
494    let Some(tree) = parse_python_tree(source) else {
495        return Vec::new();
496    };
497    let mut bound: HashMap<String, PathBuf> = HashMap::default();
498    bound.insert("__file__".to_owned(), marker_rel_path.to_path_buf());
499    let mut appended: Vec<PathBuf> = Vec::new();
500    walk_named_tree_preorder(tree.root_node(), true, |node| {
501        match node.kind() {
502            // Only module-level code runs the extension, so a name bound inside
503            // a function or class body says nothing about what `__path__`
504            // holds after the marker is executed.
505            "function_definition" | "class_definition" | "lambda" => {
506                return WalkControl::SkipChildren;
507            }
508            "assignment" => {
509                // A marker runs from the top, so a statement that assigns
510                // `__path__` throws away every directory appended before it:
511                // however many appends came first, `__path__ = [_base]` is the
512                // list the imports that follow will search.
513                if python_names_path(node.child_by_field_name("left"), source) {
514                    appended.clear();
515                }
516                python_bind_assignment(node, source, &mut bound);
517            }
518            "call" => {
519                let directories = python_path_appended_dirs_in_call(node, source, &bound);
520                if directories.is_empty()
521                    || !python_extension_statement_runs(node, source, &bound, &directories)
522                {
523                    return WalkControl::Continue;
524                }
525                for directory in directories {
526                    if directory.is_relative()
527                        && !directory.as_os_str().is_empty()
528                        && !appended.contains(&directory)
529                    {
530                        appended.push(directory);
531                    }
532                }
533            }
534            _ => {}
535        }
536        WalkControl::Continue
537    });
538    appended
539}
540
541/// Whether `node` -- when it is one -- is the marker's own `__path__` name.
542fn python_names_path(node: Option<Node<'_>>, source: &str) -> bool {
543    node.is_some_and(|node| {
544        node.kind() == "identifier" && node.utf8_text(source.as_bytes()).ok() == Some("__path__")
545    })
546}
547
548/// Whether the statement holding one `__path__` extension call is one the
549/// interpreter runs before the imports the marker serves.
550///
551/// Module-level statements run. Inside a branch they run only when the branch
552/// tests exactly the fact that makes the extension necessary: the pinned
553/// shim's membership guard `if _impl not in __path__:`, whose tested name
554/// denotes the directory the branch appends. Every other branch shape is not
555/// evidence -- `if False:` never runs the append, a predicate the reader
556/// cannot answer says nothing, and a loop, `try` or `else` body is no more
557/// proven than a comparison the guard does not state -- so the marker leaves
558/// an honest resolution gap rather than a binding. The same rule applies to
559/// the guard's own position: the append runs only when the guard does, so a
560/// guard that is itself inside another branch -- `if False:` around the pinned
561/// membership guard, say -- states nothing and leaves the gap (#3506).
562fn python_extension_statement_runs(
563    call: Node<'_>,
564    source: &str,
565    bound: &HashMap<String, PathBuf>,
566    directories: &[PathBuf],
567) -> bool {
568    let Some(statement) = call.parent() else {
569        return false;
570    };
571    if statement.kind() != "expression_statement" {
572        return false;
573    }
574    let Some(container) = statement.parent() else {
575        return false;
576    };
577    if container.kind() == "module" {
578        return true;
579    }
580    // The call's statement is a guarded body only when its own block is that
581    // `if`'s consequence: `else`, `elif`, loop, `try` and `with` bodies all
582    // share the block shape, and none of them is this guard. A one-line
583    // `if x: y()` puts the statement directly under the `if` instead.
584    let guarded = if container.kind() == "block" {
585        container.parent().filter(|parent| {
586            parent.kind() == "if_statement"
587                && parent.child_by_field_name("consequence") == Some(container)
588        })
589    } else if container.kind() == "if_statement"
590        && container.child_by_field_name("consequence") == Some(statement)
591    {
592        Some(container)
593    } else {
594        None
595    };
596    let Some(guarded) = guarded else {
597        return false;
598    };
599    // Execution certainty has to hold for the guard's ancestors too, and the
600    // only enclosing scope this reader can vouch for is the marker's own body:
601    // a guard under another branch, loop, `with` or `try` is no more proven
602    // than the shape that holds it.
603    if guarded.parent().map(|parent| parent.kind()) != Some("module") {
604        return false;
605    }
606    let Some(condition) = guarded.child_by_field_name("condition") else {
607        return false;
608    };
609    python_condition_justifies_path_extension(condition, source, bound, directories)
610}
611
612/// Whether a branch condition tests exactly the fact that makes the extension
613/// necessary: the directory the branch appends is the one missing from
614/// `__path__`, which is what the pinned shim's `if _impl not in __path__:`
615/// states.
616fn python_condition_justifies_path_extension(
617    condition: Node<'_>,
618    source: &str,
619    bound: &HashMap<String, PathBuf>,
620    directories: &[PathBuf],
621) -> bool {
622    if condition.kind() != "comparison_operator" {
623        return false;
624    }
625    let mut cursor = condition.walk();
626    let operators = condition
627        .children_by_field_name("operators", &mut cursor)
628        .map(|operator| operator.kind())
629        .collect::<Vec<_>>();
630    if operators.as_slice() != ["not in"] {
631        return false;
632    }
633    // The grammar gives a comparison no operand fields: its named children are
634    // the operands in source order.
635    let mut cursor = condition.walk();
636    let operands: Vec<Node<'_>> = condition.named_children(&mut cursor).collect();
637    let [left, right] = operands.as_slice() else {
638        return false;
639    };
640    if !python_names_path(Some(*right), source) {
641        return false;
642    }
643    python_path_expression_dir(*left, source, bound)
644        .is_some_and(|directory| directories.contains(&directory))
645}
646
647/// Bind the names one `name = value` statement assigns -- and every name of a
648/// chained `a = b = value` -- to the directory the value denotes. A value no
649/// shape answers leaves the name unbound, so a later use of it contributes no
650/// evidence either.
651fn python_bind_assignment(
652    assignment: Node<'_>,
653    source: &str,
654    bound: &mut HashMap<String, PathBuf>,
655) {
656    let mut targets: Vec<Node<'_>> = Vec::new();
657    let mut current = assignment;
658    loop {
659        let (Some(left), Some(right)) = (
660            current.child_by_field_name("left"),
661            current.child_by_field_name("right"),
662        ) else {
663            return;
664        };
665        targets.push(left);
666        if right.kind() != "assignment" {
667            let resolved = python_path_expression_dir(right, source, bound);
668            for target in targets {
669                if target.kind() != "identifier" {
670                    continue;
671                }
672                let Ok(name) = target.utf8_text(source.as_bytes()) else {
673                    continue;
674                };
675                match &resolved {
676                    Some(directory) => {
677                        bound.insert(name.to_owned(), directory.clone());
678                    }
679                    None => {
680                        bound.remove(name);
681                    }
682                }
683            }
684            return;
685        }
686        current = right;
687    }
688}
689
690/// The directories one `__path__.append(...)`, `__path__.insert(...)` or
691/// `__path__.extend(...)` call adds.
692///
693/// The call has to be a method of the marker's own `__path__`, which is the
694/// name the interpreter gives the search path it is building; a same-named
695/// attribute of anything else is not that list.
696fn python_path_appended_dirs_in_call(
697    call: Node<'_>,
698    source: &str,
699    bound: &HashMap<String, PathBuf>,
700) -> Vec<PathBuf> {
701    let Some(function) = call.child_by_field_name("function") else {
702        return Vec::new();
703    };
704    let Some(path) = python_static_attribute_path(function) else {
705        return Vec::new();
706    };
707    let [object, method] = path.as_slice() else {
708        return Vec::new();
709    };
710    if object.utf8_text(source.as_bytes()).ok() != Some("__path__") {
711        return Vec::new();
712    }
713    let Ok(method) = method.utf8_text(source.as_bytes()) else {
714        return Vec::new();
715    };
716    let Some(arguments) = call.child_by_field_name("arguments") else {
717        return Vec::new();
718    };
719    let mut cursor = arguments.walk();
720    let arguments: Vec<Node<'_>> = arguments.named_children(&mut cursor).collect();
721    let named: Vec<Node<'_>> = match method {
722        "append" => arguments.first().copied().into_iter().collect(),
723        "insert" => arguments.get(1).copied().into_iter().collect(),
724        "extend" => match arguments.first() {
725            Some(list) if list.kind() == "list" => {
726                let mut cursor = list.walk();
727                list.named_children(&mut cursor).collect()
728            }
729            Some(iterator) => vec![*iterator],
730            None => Vec::new(),
731        },
732        _ => Vec::new(),
733    };
734    named
735        .into_iter()
736        .filter_map(|node| python_path_expression_dir(node, source, bound))
737        .collect()
738}
739
740/// The project-relative directory a marker's own expression denotes under the
741/// names it has bound so far: a plain string literal, a bound name, or the
742/// `os.path` call that computes a directory from them.
743fn python_path_expression_dir(
744    node: Node<'_>,
745    source: &str,
746    bound: &HashMap<String, PathBuf>,
747) -> Option<PathBuf> {
748    if let Some(literal) = python_plain_string_literal(node, source) {
749        let directory = PathBuf::from(literal);
750        return (!directory.as_os_str().is_empty()).then_some(directory);
751    }
752    match node.kind() {
753        "identifier" => bound.get(node.utf8_text(source.as_bytes()).ok()?).cloned(),
754        "call" => python_path_call_dir(node, source, bound),
755        _ => None,
756    }
757}
758
759/// The directory one `os.path.dirname(...)` or `os.path.join(...)` call
760/// computes.
761fn python_path_call_dir(
762    call: Node<'_>,
763    source: &str,
764    bound: &HashMap<String, PathBuf>,
765) -> Option<PathBuf> {
766    let path = python_static_attribute_path(call.child_by_field_name("function")?)?;
767    let names: Vec<&str> = path
768        .iter()
769        .map(|node| node.utf8_text(source.as_bytes()).ok())
770        .collect::<Option<Vec<_>>>()?;
771    let arguments = call.child_by_field_name("arguments")?;
772    let mut cursor = arguments.walk();
773    let arguments: Vec<Node<'_>> = arguments.named_children(&mut cursor).collect();
774    match names.as_slice() {
775        ["os", "path", "dirname"] => {
776            let directory = python_path_expression_dir(*arguments.first()?, source, bound)?;
777            Some(directory.parent()?.to_path_buf())
778        }
779        ["os", "path", "join"] => {
780            let mut parts = arguments.iter();
781            let mut joined = python_path_expression_dir(*parts.next()?, source, bound)?;
782            for part in parts {
783                joined.push(python_path_expression_dir(*part, source, bound)?);
784            }
785            Some(joined)
786        }
787        _ => None,
788    }
789}
790
791/// The module `module_fq` names, as written in `importer`.
792///
793/// `importer` is `None` for an FQN-level question that no import statement
794/// asked -- a dotted name looked up on its own has no import root to be
795/// resolved against. With an importer the candidates are narrowed by
796/// [`retain_modules_for_importer`] first, and a specifier the index holds under
797/// no name at all falls through to [`module_under_source_root`].
798pub fn resolve_module_code_unit(
799    python: &dyn PythonSource,
800    importer: Option<&ProjectFile>,
801    module_fq: &str,
802) -> Option<CodeUnit> {
803    let indexed = match python.path_module_fqn(module_fq) {
804        // A path lookup that succeeds but finds no module unit is an answer:
805        // it does not fall through to the definition lookup.
806        Some(units) => indexed_module_for_importer(importer, units),
807        None => indexed_module_for_importer(importer, python.definition_fqn(module_fq)),
808    };
809    match indexed {
810        IndexedModuleLookup::Unique(module) => Some(module),
811        // The index spells the specifier more than once and the workspace does
812        // not say which of them the import means. A tail match elsewhere names
813        // a different module, so it cannot break this tie.
814        IndexedModuleLookup::Ambiguous => None,
815        IndexedModuleLookup::Missing => {
816            let importer = importer?;
817            module_under_source_root(&python.module_spellings(), importer, module_fq)
818        }
819    }
820}
821
822/// What the exact index says about a module specifier, before any source-root
823/// inference runs.
824///
825/// The three cases are kept apart because only one of them may fall through to
826/// the source-root candidates. A specifier the index spells exactly but several
827/// times is an ambiguity of identity (#3475): answering it with a suffix
828/// candidate that happens to end in the same name would replace a module the
829/// import names with an unrelated one the workspace never offered.
830enum IndexedModuleLookup {
831    Unique(CodeUnit),
832    Ambiguous,
833    Missing,
834}
835
836fn indexed_module_for_importer(
837    importer: Option<&ProjectFile>,
838    units: Vec<CodeUnit>,
839) -> IndexedModuleLookup {
840    let mut modules = units
841        .into_iter()
842        .filter(CodeUnit::is_module)
843        .collect::<Vec<_>>();
844    if let Some(importer) = importer {
845        retain_modules_for_importer(importer, &mut modules, CodeUnit::source);
846    }
847    match modules.len() {
848        0 => IndexedModuleLookup::Missing,
849        1 => IndexedModuleLookup::Unique(modules.remove(0)),
850        _ => IndexedModuleLookup::Ambiguous,
851    }
852}
853
854/// The file an absolute `module_fq` names when it is written against a source
855/// root the workspace's own structure establishes.
856///
857/// Python resolves an absolute import against the `sys.path` entries the
858/// importing program runs with. A workspace states the project root and its
859/// packaging roots, and [`python_import_root`] names modules from those; it does
860/// not state an entry only `PYTHONPATH` adds -- dayu's `dependency/`, whose
861/// `core.lib.common` is indexed as `dependency.core.lib.common` -- and a
862/// distribution directory holding a same-named package makes the inner package
863/// importable under both spellings, so YOLOZU's `rtdetr_pose/rtdetr_pose` is
864/// indexed as `rtdetr_pose.rtdetr_pose.config` and written as
865/// `rtdetr_pose.config` (#3506).
866///
867/// A tail match alone is not that evidence: `source/realpkg/vendor/api.py`
868/// ends in `vendor/api.py`, but under `where = ["source"]` it is
869/// `realpkg.vendor.api` and nothing else. What establishes a root is the
870/// snapshot's own structure -- a packaging manifest that declares it, or a
871/// complete `__init__.py` package chain from it down to the module -- and only
872/// a candidate whose implied root is established that way binds.
873/// [`PythonModuleSpellings::establishes_source_root`] is that decision; the
874/// candidates it rejects stay visible to the diagnostics as the spellings the
875/// workspace does index, which is a resolution gap and not externality.
876///
877/// Uniqueness is required after the same nearest-root narrowing an indexed
878/// module gets: two workspace copies of one module are an ambiguity the
879/// workspace does not resolve, and choosing between them here would invent a
880/// preference it never stated (#3475's rule, applied to the roots it cannot
881/// see).
882pub fn python_module_file_under_source_root(
883    spellings: &PythonModuleSpellings,
884    importer: &ProjectFile,
885    module_fq: &str,
886) -> Option<ProjectFile> {
887    let mut candidates = spellings.suffix_candidates(module_fq);
888    retain_nearest_owning_root(importer, &mut candidates, |candidate| {
889        candidate.source_root.clone()
890    });
891    candidates.retain(|candidate| spellings.establishes_source_root(candidate));
892    let [candidate] = candidates.as_slice() else {
893        return None;
894    };
895    debug_assert_eq!(
896        importer.root(),
897        candidate.file.root(),
898        "one workspace's Python files share one project root"
899    );
900    Some(candidate.file.clone())
901}
902
903/// [`python_module_file_under_source_root`] as the module unit the store-backed
904/// resolvers answer with. The unit is built from the file's own path, which is
905/// where its identity comes from, so it is the same unit the path-symbol index
906/// holds for that file.
907fn module_under_source_root(
908    spellings: &PythonModuleSpellings,
909    importer: &ProjectFile,
910    module_fq: &str,
911) -> Option<CodeUnit> {
912    let file = python_module_file_under_source_root(spellings, importer, module_fq)?;
913    module_code_unit(&file, &python_module_name(&file))
914}
915
916/// Batched sibling of `resolve_module_code_unit`: resolves every FQN's path-symbol lookup in one
917/// store transaction instead of one per FQN, then falls back to the (unbatched, rarer)
918/// definition-lookup path per FQN exactly as the single-FQN version does. Preserves its per-item
919/// semantics precisely, including that a path lookup which succeeds but finds no module unit does
920/// *not* fall through to the definition lookup.
921pub fn resolve_module_code_units_batch(
922    python: &dyn PythonSource,
923    importer: Option<&ProjectFile>,
924    module_fqs: &[String],
925) -> Vec<Option<CodeUnit>> {
926    let path_results = python.path_module_fqns_batch(module_fqs);
927    let mut lookups: Vec<IndexedModuleLookup> = Vec::with_capacity(module_fqs.len());
928    let mut needs_definition_fallback = Vec::new();
929    for (i, units) in path_results.into_iter().enumerate() {
930        match units {
931            Some(units) => lookups.push(indexed_module_for_importer(importer, units)),
932            None => {
933                needs_definition_fallback.push(i);
934                // Placeholder, replaced by the definition lookup below.
935                lookups.push(IndexedModuleLookup::Missing);
936            }
937        }
938    }
939    for i in needs_definition_fallback {
940        lookups[i] = indexed_module_for_importer(importer, python.definition_fqn(&module_fqs[i]));
941    }
942    let mut results: Vec<Option<CodeUnit>> = vec![None; module_fqs.len()];
943    let mut needs_source_root = Vec::new();
944    for (i, lookup) in lookups.into_iter().enumerate() {
945        match lookup {
946            IndexedModuleLookup::Unique(module) => results[i] = Some(module),
947            IndexedModuleLookup::Ambiguous => {}
948            IndexedModuleLookup::Missing => needs_source_root.push(i),
949        }
950    }
951    if let Some(importer) = importer
952        && !needs_source_root.is_empty()
953    {
954        // One memo handle for the whole batch: a workspace's external imports
955        // all miss the index, so this arm runs for most of the batch.
956        let spellings = python.module_spellings();
957        for i in needs_source_root {
958            results[i] = module_under_source_root(&spellings, importer, &module_fqs[i]);
959        }
960    }
961    results
962}
963
964pub fn compute_export_index_of(
965    python: &dyn PythonSource,
966    token: QueryToken<'_>,
967    file: &ProjectFile,
968) -> ExportIndex {
969    let mut index = ExportIndex::empty();
970    let mut events = Vec::new();
971    let declarations = python.top_level_declarations(file);
972    collect_local_export_events(
973        declarations.iter(),
974        |code_unit| {
975            python
976                .ranges(code_unit)
977                .iter()
978                .map(|range| range.start_byte)
979                .min()
980                .unwrap_or(usize::MAX)
981        },
982        &mut events,
983    );
984
985    let imports = python.import_info_of(token, file);
986    collect_reexport_events_from_imports(python, file, &imports, &mut events, &mut index);
987
988    finish_export_index(events, index)
989}
990
991pub fn export_index_from_file_facts(
992    python: &dyn PythonSource,
993    file: &ProjectFile,
994    facts: &dyn IndexedFileFacts,
995    module_name: &str,
996) -> ExportIndex {
997    let mut index = ExportIndex::empty();
998    let mut events = Vec::new();
999    collect_local_export_events(
1000        facts.top_level_declarations().iter(),
1001        |code_unit| {
1002            facts
1003                .declaration_ranges(code_unit)
1004                .into_iter()
1005                .flatten()
1006                .map(|range| range.start_byte)
1007                .min()
1008                .unwrap_or(usize::MAX)
1009        },
1010        &mut events,
1011    );
1012
1013    if !facts
1014        .top_level_declarations()
1015        .iter()
1016        .any(CodeUnit::is_module)
1017        && let Some(identifier) = module_name.rsplit('.').next()
1018        && !identifier.is_empty()
1019        && !identifier.starts_with('_')
1020    {
1021        events.push((
1022            0,
1023            identifier.to_string(),
1024            ExportEntry::Local {
1025                local_name: identifier.to_string(),
1026            },
1027        ));
1028    }
1029
1030    collect_reexport_events_from_imports(python, file, facts.imports(), &mut events, &mut index);
1031
1032    finish_export_index(events, index)
1033}
1034
1035fn collect_local_export_events<'a>(
1036    declarations: impl IntoIterator<Item = &'a CodeUnit>,
1037    mut start_byte: impl FnMut(&CodeUnit) -> usize,
1038    events: &mut Vec<(usize, String, ExportEntry)>,
1039) -> HashSet<String> {
1040    let mut local_names = HashSet::default();
1041    for code_unit in declarations {
1042        let identifier = code_unit.identifier().trim();
1043        if identifier.is_empty() {
1044            continue;
1045        }
1046        local_names.insert(identifier.to_string());
1047        events.push((
1048            start_byte(code_unit),
1049            identifier.to_string(),
1050            ExportEntry::Local {
1051                local_name: identifier.to_string(),
1052            },
1053        ));
1054    }
1055    local_names
1056}
1057
1058fn finish_export_index(
1059    mut events: Vec<(usize, String, ExportEntry)>,
1060    mut index: ExportIndex,
1061) -> ExportIndex {
1062    events.sort_by_key(|(start_byte, _, _)| *start_byte);
1063    for (_, exported_name, entry) in events {
1064        index.exports_by_name.insert(exported_name, entry);
1065    }
1066    index
1067}
1068
1069fn collect_reexport_events_from_imports(
1070    python: &dyn PythonSource,
1071    file: &ProjectFile,
1072    imports: &[ImportInfo],
1073    events: &mut Vec<(usize, String, ExportEntry)>,
1074    index: &mut ExportIndex,
1075) {
1076    for import in imports {
1077        if import
1078            .path
1079            .as_ref()
1080            .is_none_or(|path| !path.lexical_scopes.is_empty())
1081        {
1082            continue;
1083        }
1084        record_single_reexport_event(python, file, import, events, index);
1085    }
1086}
1087
1088fn record_single_reexport_event(
1089    python: &dyn PythonSource,
1090    file: &ProjectFile,
1091    import: &ImportInfo,
1092    events: &mut Vec<(usize, String, ExportEntry)>,
1093    index: &mut ExportIndex,
1094) {
1095    let Some(PythonImportDetails::FromImport {
1096        module,
1097        name,
1098        alias,
1099        wildcard,
1100    }) = python_import_details(import)
1101    else {
1102        return;
1103    };
1104    let start_byte = import
1105        .path
1106        .as_ref()
1107        .map(|path| path.declaration_start_byte)
1108        .unwrap_or(usize::MAX);
1109    let resolved_module = if module.starts_with('.') {
1110        resolve_python_relative_module(file, &module)
1111    } else {
1112        Some(module.clone())
1113    };
1114    let Some(resolved_module) = resolved_module else {
1115        return;
1116    };
1117
1118    if wildcard {
1119        index.reexport_stars.push(ReexportStar {
1120            module_specifier: resolved_module,
1121        });
1122        return;
1123    }
1124    let exported_name = alias.unwrap_or(name.clone());
1125    // `from P import S` binds the submodule `P.S` itself when that module
1126    // exists, exactly as the import binder reads it below. Recording it as
1127    // "the name S inside module P.S" would follow the subpackage's own
1128    // exports, which silently mis-resolves whenever the subpackage re-exports
1129    // a member named after itself (issue #1762).
1130    let module_candidate = format!("{resolved_module}.{name}");
1131    if resolve_module_code_unit(python, Some(file), &module_candidate).is_some() {
1132        events.push((
1133            start_byte,
1134            exported_name,
1135            ExportEntry::ReexportedModule {
1136                module_specifier: module_candidate,
1137            },
1138        ));
1139        return;
1140    }
1141    events.push((
1142        start_byte,
1143        exported_name,
1144        ExportEntry::ReexportedNamed {
1145            module_specifier: resolved_module,
1146            imported_name: name,
1147        },
1148    ));
1149}
1150
1151pub fn import_binder_from_imports(
1152    python: &dyn PythonSource,
1153    file: &ProjectFile,
1154    imports: &[ImportInfo],
1155) -> ImportBinder {
1156    let mut binder = ImportBinder::empty();
1157
1158    for (local_name, binding) in import_bindings_from_imports(python, file, imports) {
1159        binder.bindings.insert(local_name, binding);
1160    }
1161
1162    binder
1163}
1164
1165/// Resolve each structured import without collapsing repeated local names.
1166///
1167/// Candidate discovery needs every lexical binding. The ordinary binder keeps
1168/// one effective binding for simple lookups.
1169pub fn import_bindings_from_imports(
1170    python: &dyn PythonSource,
1171    file: &ProjectFile,
1172    imports: &[ImportInfo],
1173) -> Vec<(String, ImportBinding)> {
1174    let mut bindings = Vec::new();
1175
1176    for import in imports {
1177        let Some(details) = python_import_details(import) else {
1178            continue;
1179        };
1180        match details {
1181            PythonImportDetails::Import { module, alias } => {
1182                let local_name = python_namespace_binding_name(import, alias.as_deref(), &module);
1183                let module_specifier =
1184                    python_namespace_binding_module(import, alias.as_deref(), &module);
1185                bindings.push((
1186                    local_name,
1187                    ImportBinding {
1188                        module_specifier,
1189                        namespace_imported_module: Some(module),
1190                        kind: ImportKind::Namespace,
1191                        imported_name: None,
1192                    },
1193                ));
1194            }
1195            PythonImportDetails::FromImport {
1196                module,
1197                name,
1198                wildcard,
1199                ..
1200            } => {
1201                let resolved_module = if module.starts_with('.') {
1202                    resolve_python_relative_module(file, &module)
1203                } else {
1204                    Some(module.clone())
1205                };
1206                let Some(resolved_module) = resolved_module else {
1207                    continue;
1208                };
1209                if wildcard {
1210                    // A glob import introduces each public declaration as a
1211                    // real local binding. Expand it from the structured module
1212                    // declarations so constructor and receiver inference can
1213                    // resolve the same names Python places in the namespace.
1214                    bindings.extend(
1215                        public_declarations_in_module(python, file, &resolved_module)
1216                            .into_iter()
1217                            .map(|declaration| {
1218                                let name = declaration.identifier().to_string();
1219                                (
1220                                    name.clone(),
1221                                    ImportBinding {
1222                                        module_specifier: resolved_module.clone(),
1223                                        namespace_imported_module: None,
1224                                        kind: ImportKind::Named,
1225                                        imported_name: Some(name),
1226                                    },
1227                                )
1228                            }),
1229                    );
1230                    continue;
1231                }
1232                // Non-wildcard from-imports always populate `identifier`
1233                // as `alias ?? name` (see `python_import_details`), so
1234                // `local_name()` reproduces the same alias-first fallback
1235                // without re-deriving it here.
1236                let local_name = import
1237                    .local_name()
1238                    .map(str::to_string)
1239                    .unwrap_or_else(|| name.clone());
1240                let module_candidate = format!("{resolved_module}.{name}");
1241                if resolve_module_code_unit(python, Some(file), &module_candidate).is_some() {
1242                    bindings.push((
1243                        local_name,
1244                        ImportBinding {
1245                            module_specifier: module_candidate,
1246                            namespace_imported_module: None,
1247                            kind: ImportKind::Namespace,
1248                            imported_name: None,
1249                        },
1250                    ));
1251                    continue;
1252                }
1253                bindings.push((
1254                    local_name,
1255                    ImportBinding {
1256                        module_specifier: resolved_module,
1257                        namespace_imported_module: None,
1258                        kind: ImportKind::Named,
1259                        imported_name: Some(name),
1260                    },
1261                ));
1262            }
1263        }
1264    }
1265
1266    bindings
1267}
1268
1269pub fn public_declarations_in_module(
1270    python: &dyn PythonSource,
1271    importer: &ProjectFile,
1272    module_fq: &str,
1273) -> Vec<CodeUnit> {
1274    let Some(module_code_unit) = resolve_module_code_unit(python, Some(importer), module_fq) else {
1275        return Vec::new();
1276    };
1277    python
1278        .direct_children(&module_code_unit)
1279        .into_iter()
1280        .filter(|code_unit| !code_unit.identifier().starts_with('_'))
1281        .collect()
1282}
1283
1284pub fn resolve_base_class(
1285    python: &dyn PythonSource,
1286    token: QueryToken<'_>,
1287    code_unit: &CodeUnit,
1288    raw: &str,
1289) -> Option<CodeUnit> {
1290    let trimmed = raw.trim();
1291    if trimmed.is_empty() {
1292        return None;
1293    }
1294
1295    let binder = python.import_binder_of(code_unit.source());
1296    if let Some((head, tail)) = trimmed.split_once('.') {
1297        if let Some(binding) = binder.bindings.get(head)
1298            && binding.kind == ImportKind::Namespace
1299        {
1300            let fq_name = format!("{}.{}", binding.module_specifier, tail);
1301            return python.definitions(&fq_name).next();
1302        }
1303        return python.definitions(trimmed).next();
1304    }
1305
1306    if let Some(binding) = binder.bindings.get(trimmed) {
1307        match binding.kind {
1308            ImportKind::Namespace => {
1309                return resolve_module_code_unit(
1310                    python,
1311                    Some(code_unit.source()),
1312                    &binding.module_specifier,
1313                );
1314            }
1315            ImportKind::Named => {
1316                let imported_name = binding.imported_name.as_ref()?;
1317                let fqn = format!("{}.{}", binding.module_specifier, imported_name);
1318                return resolve_exported_fqn(python, Some(code_unit.source()), &fqn)
1319                    .into_iter()
1320                    .next()
1321                    .or_else(|| python.definitions(&fqn).next());
1322            }
1323            _ => {}
1324        }
1325    }
1326
1327    if python
1328        .import_info_of(token, code_unit.source())
1329        .iter()
1330        .any(|import| import.is_wildcard)
1331        && let Some(imported) =
1332            resolve_import_bindings(python, token, code_unit.source()).get(trimmed)
1333    {
1334        return Some(imported.clone());
1335    }
1336
1337    let local_fq_name = format!("{}.{}", code_unit.package_name(), trimmed);
1338    python
1339        .definitions(&local_fq_name)
1340        .next()
1341        .or_else(|| python.definitions(trimmed).next())
1342}
1343
1344pub fn render_skeleton_recursive(
1345    index: &dyn CodeUnitIndex,
1346    code_unit: &CodeUnit,
1347    indent: &str,
1348    header_only: bool,
1349    out: &mut String,
1350) {
1351    enum Work {
1352        Declaration(CodeUnit, String),
1353        Elision(String),
1354    }
1355    let mut stack = vec![Work::Declaration(code_unit.clone(), indent.to_string())];
1356    while let Some(work) = stack.pop() {
1357        let Work::Declaration(unit, indent) = work else {
1358            let Work::Elision(indent) = work else {
1359                unreachable!()
1360            };
1361            out.push_str(&indent);
1362            out.push_str("[...]\n");
1363            continue;
1364        };
1365        if !unit.is_module()
1366            && !unit.is_file_scope()
1367            && let Some(signature) = index.signatures(&unit).first()
1368        {
1369            // A property's declaration label is callable-shaped, but the
1370            // skeleton has always displayed its header as a field.
1371            let signature = if unit.is_field() {
1372                signature.strip_suffix(" ...").unwrap_or(signature)
1373            } else {
1374                signature
1375            };
1376            for line in signature.lines() {
1377                out.push_str(&indent);
1378                out.push_str(line);
1379                out.push('\n');
1380            }
1381        }
1382        let children = index.direct_children(&unit);
1383        let child_indent = format!("{indent}  ");
1384        if header_only && children.iter().any(|child| !child.is_field()) {
1385            stack.push(Work::Elision(child_indent.clone()));
1386        }
1387        for child in children
1388            .into_iter()
1389            .rev()
1390            .filter(|child| !header_only || child.is_field())
1391        {
1392            stack.push(Work::Declaration(child, child_indent.clone()));
1393        }
1394    }
1395}