use tree_sitter::{Node, Tree};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ImportedName {
Default,
Namespace,
Named(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BindingKind {
Const,
Let,
Var,
Param,
Function,
Class,
CatchParam,
Assignment,
Loop,
ContextManager,
Comprehension,
Type,
Receiver,
TypeParam,
Module,
Trait,
}
impl BindingKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Const => "const",
Self::Let => "let",
Self::Var => "var",
Self::Param => "param",
Self::Function => "function",
Self::Class => "class",
Self::CatchParam => "catch-param",
Self::Assignment => "assignment",
Self::Loop => "loop",
Self::ContextManager => "context-manager",
Self::Comprehension => "comprehension",
Self::Type => "type",
Self::Receiver => "receiver",
Self::TypeParam => "type-param",
Self::Module => "module",
Self::Trait => "trait",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Binding {
Import {
module: String,
name: ImportedName,
},
Local(BindingKind),
}
impl Binding {
#[must_use]
pub const fn kind_str(&self) -> &'static str {
match self {
Self::Import { .. } => "import",
Self::Local(kind) => kind.as_str(),
}
}
#[must_use]
pub fn is_import_of(&self, module: &str, name: Option<&str>) -> bool {
let Self::Import {
module: from,
name: imported,
} = self
else {
return false;
};
from.as_str() == module
&& name.is_none_or(|wanted| match imported {
ImportedName::Named(actual) => actual.as_str() == wanted,
ImportedName::Default => wanted == "default",
ImportedName::Namespace => wanted == "*",
})
}
#[must_use]
pub fn is_imported_from(&self, pattern: &str) -> bool {
match self {
Self::Import { module, .. } => glob_matches(pattern, module),
Self::Local(_) => false,
}
}
}
fn glob_matches(pattern: &str, text: &str) -> bool {
let mut parts = pattern.split('*');
let Some(first) = parts.next() else {
return true;
};
if !text.starts_with(first) {
return false;
}
let mut rest = &text[first.len()..];
let segments: Vec<&str> = parts.collect();
if segments.is_empty() {
return rest.is_empty();
}
for (index, segment) in segments.iter().enumerate() {
if segment.is_empty() {
continue;
}
if index == segments.len() - 1 {
return rest.ends_with(segment);
}
match rest.find(segment) {
Some(at) => rest = &rest[at + segment.len()..],
None => return false,
}
}
true
}
pub trait BindingResolver: Send + Sync {
fn resolve(&self, tree: &Tree, source: &str, node: Node<'_>) -> Option<Binding>;
fn is_shadowed(&self, tree: &Tree, source: &str, node: Node<'_>) -> bool;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn binding_kinds_render_as_rules_write_them() {
assert_eq!(BindingKind::Const.as_str(), "const");
assert_eq!(BindingKind::Param.as_str(), "param");
assert_eq!(BindingKind::CatchParam.as_str(), "catch-param");
}
#[test]
fn imports_report_as_import_regardless_of_which_export() {
for name in [
ImportedName::Default,
ImportedName::Namespace,
ImportedName::Named("a".to_owned()),
] {
let binding = Binding::Import {
module: "m".to_owned(),
name,
};
assert_eq!(binding.kind_str(), "import");
}
}
#[test]
fn local_bindings_report_their_own_kind() {
assert_eq!(Binding::Local(BindingKind::Const).kind_str(), "const");
assert_eq!(Binding::Local(BindingKind::Function).kind_str(), "function");
}
fn named_import(module: &str, name: &str) -> Binding {
Binding::Import {
module: module.to_owned(),
name: ImportedName::Named(name.to_owned()),
}
}
#[test]
fn an_import_matches_its_own_module_and_export() {
let binding = named_import("@rneui/themed", "makeStyles");
assert!(binding.is_import_of("@rneui/themed", Some("makeStyles")));
assert!(!binding.is_import_of("somewhere-else", Some("makeStyles")));
assert!(!binding.is_import_of("@rneui/themed", Some("notThatOne")));
}
#[test]
fn omitting_the_name_matches_any_export_of_the_module() {
let binding = named_import("m", "a");
assert!(binding.is_import_of("m", None));
assert!(!binding.is_import_of("other", None));
}
#[test]
fn the_default_and_namespace_forms_are_named_as_a_rule_writes_them() {
let default = Binding::Import {
module: "m".to_owned(),
name: ImportedName::Default,
};
assert!(default.is_import_of("m", Some("default")));
assert!(!default.is_import_of("m", Some("*")));
assert!(!default.is_import_of("m", Some("a")));
let namespace = Binding::Import {
module: "m".to_owned(),
name: ImportedName::Namespace,
};
assert!(namespace.is_import_of("m", Some("*")));
assert!(!namespace.is_import_of("m", Some("default")));
assert!(named_import("m", "default").is_import_of("m", Some("default")));
}
#[test]
fn a_local_binding_is_no_import_at_all() {
let local = Binding::Local(BindingKind::Const);
assert!(!local.is_import_of("m", None));
assert!(!local.is_imported_from("*"));
}
#[test]
fn glob_matching_handles_the_shapes_that_appear_in_rules() {
assert!(glob_matches("m", "m"));
assert!(!glob_matches("m", "mm"));
assert!(glob_matches("*", "anything"));
assert!(glob_matches("@scope/*", "@scope/pkg"));
assert!(!glob_matches("@scope/*", "@other/pkg"));
assert!(glob_matches("*/themed", "@rneui/themed"));
assert!(!glob_matches("*/themed", "@rneui/other"));
assert!(glob_matches("@a/*/c", "@a/b/c"));
assert!(!glob_matches("@a/*/c", "@a/b/d"));
assert!(glob_matches("", ""));
assert!(!glob_matches("", "x"));
}
#[test]
fn an_import_is_matched_by_a_glob_over_its_module() {
let binding = named_import("@scope/pkg", "a");
assert!(binding.is_imported_from("@scope/*"));
assert!(binding.is_imported_from("*/pkg"));
assert!(binding.is_imported_from("@scope/pkg"));
assert!(!binding.is_imported_from("@other/*"));
}
}