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}