use super::tree::ParsedFile;
use crate::diagnostic::Language;
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ResolvedName {
Imported(String),
Local,
Unbound,
}
#[derive(Default)]
pub(super) struct ImportMap {
pub(super) imports: HashMap<String, String>,
pub(super) import_lines: Vec<(usize, String)>,
pub(super) locals: HashSet<String>,
}
#[derive(Clone)]
pub(super) struct QualifiedName {
pub(super) path: String,
pub(super) terminal: String,
}
impl QualifiedName {
pub(super) fn from_path(path: String, separator: &str) -> Self {
let terminal = path
.rsplit_once(separator)
.map_or(path.as_str(), |(_, terminal)| terminal)
.to_owned();
Self { path, terminal }
}
pub(super) fn resolve(self, file: &ParsedFile) -> Option<Self> {
match resolve_name(file, &self.path) {
ResolvedName::Imported(resolved) => {
Some(Self::from_path(resolved, path_separator(file.lang())))
}
ResolvedName::Local => None,
ResolvedName::Unbound => Some(self),
}
}
}
#[must_use]
pub fn resolve_name(file: &ParsedFile, name: &str) -> ResolvedName {
let separator = path_separator(file.lang());
let (head, rest) = name
.split_once(separator)
.map_or((name, None), |(head, rest)| (head, Some(rest)));
if !is_identifier(head) {
return ResolvedName::Unbound;
}
let map = file
.imports
.get_or_init(|| dispatch_lang!(file.lang(), collect_import_map(file)));
if map.locals.contains(head) {
return ResolvedName::Local;
}
map.imports.get(head).map_or(ResolvedName::Unbound, |path| {
ResolvedName::Imported(
rest.map_or_else(|| path.clone(), |rest| format!("{path}{separator}{rest}")),
)
})
}
fn is_identifier(text: &str) -> bool {
let mut characters = text.chars();
characters
.next()
.is_some_and(|first| first == '_' || first.is_alphabetic())
&& characters.all(|character| character == '_' || character.is_alphanumeric())
}
#[must_use]
pub(super) fn import_lines(file: &ParsedFile) -> &[(usize, String)] {
&file
.imports
.get_or_init(|| dispatch_lang!(file.lang(), collect_import_map(file)))
.import_lines
}
const fn path_separator(lang: Language) -> &'static str {
match lang {
Language::Python => ".",
Language::Rust => "::",
}
}
#[cfg(test)]
mod tests {
use super::*;
fn resolve(source: &str, lang: Language, name: &str) -> ResolvedName {
resolve_name(&ParsedFile::new(source, lang), name)
}
fn imported(path: &str) -> ResolvedName {
ResolvedName::Imported(path.to_owned())
}
#[test]
fn test_python_module_alias() {
let source = "import typing as t\n";
assert_eq!(
resolve(source, Language::Python, "t.List"),
imported("typing.List")
);
assert_eq!(
resolve(source, Language::Python, "typing.List"),
ResolvedName::Unbound
);
}
#[test]
fn test_python_from_import_alias() {
let source = "from collections.abc import Set as ReadOnlySet\nfrom typing import cast\n";
assert_eq!(
resolve(source, Language::Python, "ReadOnlySet"),
imported("collections.abc.Set")
);
assert_eq!(
resolve(source, Language::Python, "cast"),
imported("typing.cast")
);
}
#[test]
fn test_python_submodule_import_binds_package() {
let source = "import os.path\nimport a.b as c\n";
assert_eq!(
resolve(source, Language::Python, "os.getenv"),
imported("os.getenv")
);
assert_eq!(resolve(source, Language::Python, "c.d"), imported("a.b.d"));
}
#[test]
fn test_python_relative_import_keeps_dots() {
let source = "from . import cast\nfrom .util import sleep\n";
assert_eq!(resolve(source, Language::Python, "cast"), imported(".cast"));
assert_eq!(
resolve(source, Language::Python, "sleep"),
imported(".util.sleep")
);
}
#[test]
fn test_python_local_definitions_shadow_imports() {
let source = indoc::indoc! {r"
from typing import cast
def cast(value):
return value
if TYPE_CHECKING:
class Set:
pass
"};
assert_eq!(
resolve(source, Language::Python, "cast"),
ResolvedName::Local
);
assert_eq!(
resolve(source, Language::Python, "Set"),
ResolvedName::Local
);
}
#[test]
fn test_python_unbound_and_nested_names() {
let source = indoc::indoc! {r"
loop = get_loop()
def run():
from gevent import sleep
sleep(1)
"};
assert_eq!(
resolve(source, Language::Python, "sleep"),
ResolvedName::Unbound
);
assert_eq!(
resolve(source, Language::Python, "loop.create_task"),
ResolvedName::Unbound
);
assert_eq!(
resolve(source, Language::Python, "get().x"),
ResolvedName::Unbound
);
}
#[test]
fn test_rust_use_tree_forms() {
let source = "use std::{env::{self, var as get_var}, thread::sleep};\nuse ::tokio::time;\n";
assert_eq!(
resolve(source, Language::Rust, "env::var"),
imported("std::env::var")
);
assert_eq!(
resolve(source, Language::Rust, "get_var"),
imported("std::env::var")
);
assert_eq!(
resolve(source, Language::Rust, "sleep"),
imported("std::thread::sleep")
);
assert_eq!(
resolve(source, Language::Rust, "time::sleep"),
imported("tokio::time::sleep")
);
}
#[test]
fn test_rust_globs_underscore_and_nested_uses_bind_nothing() {
let source = indoc::indoc! {r"
use std::thread::*;
use std::io::Write as _;
mod tests {
use std::env::var;
}
"};
assert_eq!(
resolve(source, Language::Rust, "sleep"),
ResolvedName::Unbound
);
assert_eq!(
resolve(source, Language::Rust, "Write"),
ResolvedName::Unbound
);
assert_eq!(
resolve(source, Language::Rust, "var"),
ResolvedName::Unbound
);
}
#[test]
fn test_rust_local_items_shadow_imports() {
let source = indoc::indoc! {r"
use std::thread::sleep;
fn sleep() {}
struct Config;
"};
assert_eq!(
resolve(source, Language::Rust, "sleep"),
ResolvedName::Local
);
assert_eq!(
resolve(source, Language::Rust, "Config::new"),
ResolvedName::Local
);
assert_eq!(
resolve(source, Language::Rust, "clock.sleep"),
ResolvedName::Unbound
);
}
}