use std::collections::BTreeMap;
use std::collections::HashSet;
use std::sync::Arc;
use rowan::TextSize;
use smol_str::SmolStr;
use crate::index::{ModuleIndex, PackageIndex, Visibility};
use crate::semantic::{Access, Binding, BindingId, BindingKind, LoadKind, ScopeId, SemanticModel};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Namespace {
Value,
Macro,
}
pub type ModulePath = Vec<SmolStr>;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct OccurrenceKey {
pub package: SmolStr,
pub module: ModulePath,
pub namespace: Namespace,
pub name: SmolStr,
}
pub fn module_at<'m>(root: &'m ModuleIndex, path: &[SmolStr]) -> Option<&'m ModuleIndex> {
let mut current = root;
for segment in path {
current = current
.submodules
.iter()
.find(|m| m.name == segment.as_str())?;
}
Some(current)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Resolution {
Binding(BindingId),
Workspace { module: ModulePath, name: SmolStr },
Using { module: SmolStr, name: SmolStr },
System { module: SmolStr, name: SmolStr },
Unresolved,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OccurrenceRec {
pub range: rowan::TextRange,
pub is_def: bool,
pub access: Access,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Candidate {
pub name: SmolStr,
pub source: Source,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Source {
Binding(BindingId),
Workspace {
module: SmolStr,
},
Using {
module: SmolStr,
},
System {
module: SmolStr,
},
}
pub trait PackageSource {
fn package(&self, name: &str) -> Option<Arc<PackageIndex>>;
fn package_root(&self, _name: &str) -> Option<std::path::PathBuf> {
None
}
fn workspace_member(&self, _path: &std::path::Path) -> Option<(Arc<PackageIndex>, ModulePath)> {
None
}
fn workspace_module(&self, path: &std::path::Path) -> Option<Arc<PackageIndex>> {
self.workspace_member(path).map(|(pkg, _)| pkg)
}
}
impl PackageSource for BTreeMap<String, Arc<PackageIndex>> {
fn package(&self, name: &str) -> Option<Arc<PackageIndex>> {
self.get(name).cloned()
}
}
pub struct Resolver<'a, P: PackageSource + ?Sized> {
model: &'a SemanticModel,
packages: &'a P,
workspace: Option<WorkspaceCtx>,
}
struct WorkspaceCtx {
pkg: Arc<PackageIndex>,
host: ModulePath,
}
impl<'a, P: PackageSource + ?Sized> Resolver<'a, P> {
pub fn new(model: &'a SemanticModel, packages: &'a P) -> Self {
Resolver {
model,
packages,
workspace: None,
}
}
pub fn with_workspace(mut self, workspace: Option<(Arc<PackageIndex>, ModulePath)>) -> Self {
self.workspace = workspace.map(|(pkg, host)| WorkspaceCtx { pkg, host });
self
}
fn full_module_path(&self, workspace: &WorkspaceCtx, scope: ScopeId) -> ModulePath {
let mut path = workspace.host.clone();
path.extend(self.model.enclosing_module_path(scope));
path
}
pub fn resolve(&self, name: &str, offset: TextSize, namespace: Namespace) -> Resolution {
let wanted = wanted_name(name, namespace);
if let Some(binding) = self.file_binding(&wanted, offset, namespace) {
return Resolution::Binding(binding);
}
if let Some(workspace) = &self.workspace {
let path = self.full_module_path(workspace, self.model.scope_at(offset));
if let Some(module) = module_at(&workspace.pkg.root, &path)
&& module_defines(module, &wanted, namespace)
{
return Resolution::Workspace {
module: path,
name: wanted,
};
}
}
if let Some((module, name)) = self.using_export(&wanted, offset) {
return Resolution::Using { module, name };
}
if let Some((module, name)) = self.system_export(&wanted) {
return Resolution::System { module, name };
}
Resolution::Unresolved
}
pub fn visible(&self, offset: TextSize, namespace: Namespace) -> Vec<Candidate> {
let mut seen: HashSet<SmolStr> = HashSet::new();
let mut out: Vec<Candidate> = Vec::new();
let mut cursor = Some(self.model.scope_at(offset));
while let Some(id) = cursor {
let scope = self.model.scope(id);
for &b in scope.bindings.iter().rev() {
if let Some(name) = namespaced_binding_name(self.model.binding(b), namespace)
&& seen.insert(name.clone())
{
out.push(Candidate {
name,
source: Source::Binding(b),
});
}
}
cursor = if scope.kind.is_global() {
None
} else {
scope.parent
};
}
if let Some(workspace) = &self.workspace
&& let Some(module) = module_at(
&workspace.pkg.root,
&self.full_module_path(workspace, self.model.scope_at(offset)),
)
{
for name in defined_names(module, namespace) {
if seen.insert(name.clone()) {
out.push(Candidate {
name,
source: Source::Workspace {
module: SmolStr::new(&module.name),
},
});
}
}
}
let at = self.model.scope_at(offset);
for load in self.model.module_loads() {
let Some(module) = self.using_module(load, at) else {
continue;
};
let display = load.path.components.last().unwrap().clone();
for name in exported_names(&module, namespace) {
if seen.insert(name.clone()) {
out.push(Candidate {
name,
source: Source::Using {
module: display.clone(),
},
});
}
}
}
for module in ["Base", "Core"] {
let Some(pkg) = self.packages.package(module) else {
continue;
};
for name in exported_names(&pkg.root, namespace) {
if seen.insert(name.clone()) {
out.push(Candidate {
name,
source: Source::System {
module: SmolStr::new(module),
},
});
}
}
}
out
}
pub fn workspace_occurrences(&self) -> BTreeMap<OccurrenceKey, Vec<OccurrenceRec>> {
let mut out: BTreeMap<OccurrenceKey, Vec<OccurrenceRec>> = BTreeMap::new();
let Some(workspace) = &self.workspace else {
return out;
};
for (i, binding) in self.model.bindings().iter().enumerate() {
if !self.model.scope(binding.scope).kind.is_global() {
continue;
}
let path = self.full_module_path(workspace, binding.scope);
let Some(module) = module_at(&workspace.pkg.root, &path) else {
continue;
};
for ns in [Namespace::Value, Namespace::Macro] {
let Some(name) = namespaced_binding_name(binding, ns) else {
continue;
};
if !module_defines(module, &name, ns) {
continue;
}
let recs = out
.entry(OccurrenceKey {
package: SmolStr::new(&workspace.pkg.name),
module: path.clone(),
namespace: ns,
name,
})
.or_default();
for occ in self.model.occurrences(BindingId(i as u32)) {
recs.push(OccurrenceRec {
range: occ.range,
is_def: occ.is_def,
access: occ.access,
});
}
}
}
for ident in self.model.idents() {
if ident.binding.is_some() {
continue;
}
let ns = if ident.is_macro {
Namespace::Macro
} else {
Namespace::Value
};
if let Resolution::Workspace { module, name } =
self.resolve(&ident.name, ident.range.start(), ns)
{
out.entry(OccurrenceKey {
package: SmolStr::new(&workspace.pkg.name),
module,
namespace: ns,
name,
})
.or_default()
.push(OccurrenceRec {
range: ident.range,
is_def: false,
access: ident.access,
});
}
}
for recs in out.values_mut() {
recs.sort_by_key(|r| (r.range.start(), r.range.end()));
recs.dedup_by_key(|r| (r.range.start(), r.range.end()));
}
out
}
pub fn workspace_symbol_at(&self, offset: TextSize) -> Option<OccurrenceKey> {
let workspace = self.workspace.as_ref()?;
if let Some(ident) = self.model.ident_at(offset) {
if let Some(bid) = ident.binding {
return self.binding_workspace_symbol(bid, workspace);
}
let ns = if ident.is_macro {
Namespace::Macro
} else {
Namespace::Value
};
return match self.resolve(&ident.name, offset, ns) {
Resolution::Workspace { module, name } => Some(OccurrenceKey {
package: SmolStr::new(&workspace.pkg.name),
module,
namespace: ns,
name,
}),
_ => None,
};
}
if let Some(bid) = self.model.binding_at(offset) {
return self.binding_workspace_symbol(bid, workspace);
}
None
}
fn binding_workspace_symbol(
&self,
bid: BindingId,
workspace: &WorkspaceCtx,
) -> Option<OccurrenceKey> {
let binding = self.model.binding(bid);
if !self.model.scope(binding.scope).kind.is_global() {
return None;
}
let path = self.full_module_path(workspace, binding.scope);
let module = module_at(&workspace.pkg.root, &path)?;
for ns in [Namespace::Value, Namespace::Macro] {
if let Some(name) = namespaced_binding_name(binding, ns)
&& module_defines(module, &name, ns)
{
return Some(OccurrenceKey {
package: SmolStr::new(&workspace.pkg.name),
module: path.clone(),
namespace: ns,
name,
});
}
}
None
}
fn file_binding(
&self,
wanted: &SmolStr,
offset: TextSize,
namespace: Namespace,
) -> Option<BindingId> {
let mut cursor = Some(self.model.scope_at(offset));
while let Some(id) = cursor {
let scope = self.model.scope(id);
let hit = scope.bindings.iter().rev().copied().find(|&b| {
namespaced_binding_name(self.model.binding(b), namespace).as_ref() == Some(wanted)
});
if hit.is_some() {
return hit;
}
cursor = if scope.kind.is_global() {
None
} else {
scope.parent
};
}
None
}
fn using_export(&self, wanted: &SmolStr, offset: TextSize) -> Option<(SmolStr, SmolStr)> {
let at = self.model.scope_at(offset);
for load in self.model.module_loads() {
let Some(module) = self.using_module(load, at) else {
continue;
};
if module_exports(&module, wanted) {
let display = load.path.components.last().unwrap().clone();
return Some((display, wanted.clone()));
}
}
None
}
fn system_export(&self, wanted: &SmolStr) -> Option<(SmolStr, SmolStr)> {
for module in ["Base", "Core"] {
let pkg = self.packages.package(module)?;
if module_exports(&pkg.root, wanted) {
return Some((SmolStr::new(module), wanted.clone()));
}
}
None
}
fn using_module(
&self,
load: &crate::semantic::ModuleLoad,
at: ScopeId,
) -> Option<Arc<ModuleIndexHandle>> {
if load.kind != LoadKind::Using || load.items.is_some() {
return None;
}
if load.path.leading_dots != 0 || load.path.components.is_empty() {
return None;
}
if !self.scope_visible(load.scope, at) {
return None;
}
let pkg = self.packages.package(&load.path.components[0])?;
resolve_module_path(&pkg.root, &load.path.components[1..])?;
Some(Arc::new(ModuleIndexHandle {
pkg,
rest: load.path.components[1..].to_vec(),
}))
}
fn scope_visible(&self, decl_scope: ScopeId, at: ScopeId) -> bool {
let mut cursor = Some(at);
while let Some(id) = cursor {
if id == decl_scope {
return true;
}
let scope = self.model.scope(id);
if scope.kind.is_global() {
return false;
}
cursor = scope.parent;
}
false
}
}
struct ModuleIndexHandle {
pkg: Arc<PackageIndex>,
rest: Vec<SmolStr>,
}
impl std::ops::Deref for ModuleIndexHandle {
type Target = ModuleIndex;
fn deref(&self) -> &ModuleIndex {
resolve_module_path(&self.pkg.root, &self.rest).expect("sub-path verified in using_module")
}
}
pub fn resolve_submodule<'m>(root: &'m ModuleIndex, path: &[&str]) -> Option<&'m ModuleIndex> {
let mut current = root;
for name in path {
current = current.submodules.iter().find(|m| m.name == *name)?;
}
Some(current)
}
fn resolve_module_path<'m>(root: &'m ModuleIndex, rest: &[SmolStr]) -> Option<&'m ModuleIndex> {
module_at(root, rest)
}
fn module_exports(module: &ModuleIndex, wanted: &SmolStr) -> bool {
module
.exports
.iter()
.any(|e| e.visibility == Visibility::Exported && e.name == wanted.as_str())
}
fn module_defines(module: &ModuleIndex, wanted: &SmolStr, namespace: Namespace) -> bool {
match namespace {
Namespace::Value => {
module.functions.iter().any(|f| f.name == wanted.as_str())
|| module.types.iter().any(|t| t.name == wanted.as_str())
|| module.consts.iter().any(|c| c.name == wanted.as_str())
}
Namespace::Macro => module.macros.iter().any(|m| m.name == wanted.as_str()),
}
}
fn defined_names(module: &ModuleIndex, namespace: Namespace) -> Vec<SmolStr> {
match namespace {
Namespace::Value => module
.functions
.iter()
.map(|f| f.name.as_str())
.chain(module.types.iter().map(|t| t.name.as_str()))
.chain(module.consts.iter().map(|c| c.name.as_str()))
.map(SmolStr::new)
.collect(),
Namespace::Macro => module
.macros
.iter()
.map(|m| SmolStr::new(&m.name))
.collect(),
}
}
fn exported_names(module: &ModuleIndex, namespace: Namespace) -> Vec<SmolStr> {
module
.exports
.iter()
.filter(|e| e.visibility == Visibility::Exported && in_namespace(&e.name, namespace))
.map(|e| SmolStr::new(&e.name))
.collect()
}
fn wanted_name(name: &str, namespace: Namespace) -> SmolStr {
match namespace {
Namespace::Value => SmolStr::new(name),
Namespace::Macro => SmolStr::new(format!("@{name}")),
}
}
fn in_namespace(name: &str, namespace: Namespace) -> bool {
match namespace {
Namespace::Value => !name.starts_with('@'),
Namespace::Macro => name.starts_with('@'),
}
}
fn namespaced_binding_name(binding: &Binding, namespace: Namespace) -> Option<SmolStr> {
match namespace {
Namespace::Value => match binding.kind {
BindingKind::Macro => None,
BindingKind::Import if binding.name.starts_with('@') => None,
_ => Some(binding.name.clone()),
},
Namespace::Macro => match binding.kind {
BindingKind::Macro => Some(SmolStr::new(format!("@{}", binding.name))),
BindingKind::Import if binding.name.starts_with('@') => Some(binding.name.clone()),
_ => None,
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::index::model::{DefLocation, ExportedName, Span};
fn model_of(src: &str) -> SemanticModel {
SemanticModel::build(&crate::parser::parse(src).cst)
}
fn loc() -> DefLocation {
DefLocation {
file: "src/x.jl".into(),
range: Span { start: 0, end: 0 },
}
}
fn package(name: &str, exports: &[&str]) -> Arc<PackageIndex> {
module_package(name, exports, Vec::new())
}
fn module_package(
name: &str,
exports: &[&str],
submodules: Vec<ModuleIndex>,
) -> Arc<PackageIndex> {
Arc::new(PackageIndex {
name: name.to_string(),
root: ModuleIndex {
name: name.to_string(),
bare: false,
loc: loc(),
exports: exports
.iter()
.map(|n| ExportedName {
name: n.to_string(),
visibility: Visibility::Exported,
loc: loc(),
})
.collect(),
functions: Vec::new(),
types: Vec::new(),
consts: Vec::new(),
macros: Vec::new(),
submodules,
},
members: Vec::new(),
member_modules: Default::default(),
diagnostics: Vec::new(),
})
}
fn submodule(name: &str, exports: &[&str]) -> ModuleIndex {
ModuleIndex {
name: name.to_string(),
bare: false,
loc: loc(),
exports: exports
.iter()
.map(|n| ExportedName {
name: n.to_string(),
visibility: Visibility::Exported,
loc: loc(),
})
.collect(),
functions: Vec::new(),
types: Vec::new(),
consts: Vec::new(),
macros: Vec::new(),
submodules: Vec::new(),
}
}
fn library(packages: &[Arc<PackageIndex>]) -> BTreeMap<String, Arc<PackageIndex>> {
packages
.iter()
.map(|p| (p.name.clone(), Arc::clone(p)))
.collect()
}
fn after(src: &str, needle: &str) -> TextSize {
TextSize::from((src.rfind(needle).unwrap() + needle.len()) as u32)
}
fn resolve(src: &str, name: &str, lib: &BTreeMap<String, Arc<PackageIndex>>) -> Resolution {
let model = model_of(src);
let offset = after(src, name);
Resolver::new(&model, lib).resolve(name, offset, Namespace::Value)
}
#[test]
fn local_binding_wins_over_everything() {
let lib = library(&[package("Base", &["x"]), package("A", &["x"])]);
let src = "using A\nfunction f()\n x = 1\n x\nend";
let model = model_of(src);
let offset = after(src, " x");
match Resolver::new(&model, &lib).resolve("x", offset, Namespace::Value) {
Resolution::Binding(b) => {
assert_eq!(model.binding(b).kind, BindingKind::Local);
}
other => panic!("expected the local binding, got {other:?}"),
}
}
#[test]
fn explicit_import_wins_over_using_and_base() {
let lib = library(&[package("Base", &["f"]), package("A", &["f"])]);
let src = "using A\nimport B: f\nf()";
match resolve(src, "f", &lib) {
Resolution::Binding(b) => {
let model = model_of(src);
assert_eq!(model.binding(b).kind, BindingKind::Import);
}
other => panic!("expected the explicit import, got {other:?}"),
}
}
#[test]
fn using_export_resolves_when_not_bound() {
let lib = library(&[package("A", &["greet"])]);
let src = "using A\ngreet()";
assert_eq!(
resolve(src, "greet", &lib),
Resolution::Using {
module: SmolStr::new("A"),
name: SmolStr::new("greet"),
}
);
}
#[test]
fn using_export_masks_base() {
let lib = library(&[package("Base", &["map"]), package("A", &["map"])]);
let src = "using A\nmap()";
assert_eq!(
resolve(src, "map", &lib),
Resolution::Using {
module: SmolStr::new("A"),
name: SmolStr::new("map"),
}
);
}
#[test]
fn earliest_using_wins_in_source_order() {
let lib = library(&[package("A", &["dup"]), package("B", &["dup"])]);
let src = "using B\nusing A\ndup()";
assert_eq!(
resolve(src, "dup", &lib),
Resolution::Using {
module: SmolStr::new("B"),
name: SmolStr::new("dup"),
}
);
}
#[test]
fn base_implicit_resolves_without_a_using() {
let lib = library(&[package("Base", &["println"])]);
let src = "println()";
assert_eq!(
resolve(src, "println", &lib),
Resolution::System {
module: SmolStr::new("Base"),
name: SmolStr::new("println"),
}
);
}
#[test]
fn unknown_name_is_unresolved() {
let lib = library(&[package("Base", &["println"])]);
assert_eq!(resolve("nope()", "nope", &lib), Resolution::Unresolved);
}
#[test]
fn item_using_does_not_bring_whole_module_exports() {
let lib = library(&[package("A", &["only", "other"])]);
let src = "using A: only\nother()";
assert_eq!(resolve(src, "other", &lib), Resolution::Unresolved);
}
#[test]
fn import_does_not_bring_exports() {
let lib = library(&[package("A", &["thing"])]);
let src = "import A\nthing()";
assert_eq!(resolve(src, "thing", &lib), Resolution::Unresolved);
}
#[test]
fn using_submodule_resolves_its_exports() {
let lib = library(&[module_package("A", &[], vec![submodule("B", &["inner"])])]);
let src = "using A.B\ninner()";
assert_eq!(
resolve(src, "inner", &lib),
Resolution::Using {
module: SmolStr::new("B"),
name: SmolStr::new("inner"),
}
);
}
#[test]
fn using_in_module_does_not_leak_to_file_scope() {
let lib = library(&[package("A", &["helper"])]);
let src = "module M\nusing A\nend\nhelper()";
assert_eq!(resolve(src, "helper", &lib), Resolution::Unresolved);
}
#[test]
fn file_using_does_not_reach_into_a_module_body() {
let lib = library(&[package("A", &["helper"])]);
let src = "using A\nmodule M\nhelper()\nend";
let model = model_of(src);
let offset = after(src, "helper");
assert_eq!(
Resolver::new(&model, &lib).resolve("helper", offset, Namespace::Value),
Resolution::Unresolved,
"a top-level `using` does not apply inside a nested module"
);
}
#[test]
fn relative_using_is_not_resolved_against_the_library() {
let lib = library(&[package("A", &["thing"])]);
let src = "using .A\nthing()";
assert_eq!(resolve(src, "thing", &lib), Resolution::Unresolved);
}
#[test]
fn macro_resolves_in_the_macro_namespace() {
let lib = library(&[package("Base", &["@time"])]);
let src = "@time f()";
let model = model_of(src);
let offset = after(src, "@time");
assert_eq!(
Resolver::new(&model, &lib).resolve("time", offset, Namespace::Macro),
Resolution::System {
module: SmolStr::new("Base"),
name: SmolStr::new("@time"),
}
);
}
#[test]
fn value_and_macro_namespaces_do_not_cross() {
let lib = library(&[package("Base", &["@time", "time"])]);
let src = "time\n@time f()";
let model = model_of(src);
let value_off = after(src, "time\n");
assert!(matches!(
Resolver::new(&model, &lib).resolve("time", value_off, Namespace::Value),
Resolution::System { name, .. } if name == "time"
));
}
#[test]
fn imported_macro_binding_wins_for_macro_reads() {
let lib = library(&[package("Base", &["@time"])]);
let src = "using X: @time\n@time f()";
let model = model_of(src);
let offset = after(src, "@time f");
match Resolver::new(&model, &lib).resolve("time", offset, Namespace::Macro) {
Resolution::Binding(b) => assert_eq!(model.binding(b).kind, BindingKind::Import),
other => panic!("expected the imported macro binding, got {other:?}"),
}
}
fn workspace_pkg(name: &str, functions: &[&str], macros: &[&str]) -> Arc<PackageIndex> {
use crate::index::model::{FunctionGroup, MacroDef};
let mut pkg = (*package(name, &[])).clone();
pkg.root.functions = functions
.iter()
.map(|f| FunctionGroup {
name: f.to_string(),
owner: None,
methods: Vec::new(),
doc: None,
})
.collect();
pkg.root.macros = macros
.iter()
.map(|m| MacroDef {
name: m.to_string(),
params: Vec::new(),
doc: None,
loc: loc(),
})
.collect();
Arc::new(pkg)
}
fn resolve_ws(
src: &str,
name: &str,
lib: &BTreeMap<String, Arc<PackageIndex>>,
workspace: Option<Arc<PackageIndex>>,
) -> Resolution {
let model = model_of(src);
let offset = after(src, name);
Resolver::new(&model, lib)
.with_workspace(workspace.map(|w| (w, Vec::new())))
.resolve(name, offset, Namespace::Value)
}
#[test]
fn workspace_sibling_resolves_when_free() {
let ws = workspace_pkg("MyPkg", &["bar"], &[]);
let lib = library(&[package("Base", &[])]);
assert_eq!(
resolve_ws("bar()", "bar", &lib, Some(ws)),
Resolution::Workspace {
module: Vec::new(),
name: SmolStr::new("bar")
}
);
}
#[test]
fn workspace_sibling_masks_using_and_base() {
let ws = workspace_pkg("MyPkg", &["dup"], &[]);
let lib = library(&[package("Base", &["dup"]), package("A", &["dup"])]);
assert_eq!(
resolve_ws("using A\ndup()", "dup", &lib, Some(ws)),
Resolution::Workspace {
module: Vec::new(),
name: SmolStr::new("dup")
}
);
}
#[test]
fn local_binding_still_wins_over_workspace() {
let ws = workspace_pkg("MyPkg", &["x"], &[]);
let lib = library(&[package("Base", &[])]);
let src = "function f()\n x = 1\n x\nend";
let model = model_of(src);
let offset = after(src, " x");
match Resolver::new(&model, &lib)
.with_workspace(Some((ws, Vec::new())))
.resolve("x", offset, Namespace::Value)
{
Resolution::Binding(b) => assert_eq!(model.binding(b).kind, BindingKind::Local),
other => panic!("expected the local binding, got {other:?}"),
}
}
#[test]
fn workspace_macro_resolves_in_macro_namespace() {
let ws = workspace_pkg("MyPkg", &[], &["@sib"]);
let lib = library(&[package("Base", &[])]);
let src = "@sib f()";
let model = model_of(src);
let offset = after(src, "@sib");
assert_eq!(
Resolver::new(&model, &lib)
.with_workspace(Some((ws, Vec::new())))
.resolve("sib", offset, Namespace::Macro),
Resolution::Workspace {
module: Vec::new(),
name: SmolStr::new("@sib")
}
);
}
#[test]
fn workspace_names_appear_in_completion_between_locals_and_using() {
let ws = workspace_pkg("MyPkg", &["sibling"], &[]);
let lib = library(&[package("Base", &["println"]), package("A", &["greet"])]);
let src = "using A\nfunction f(a)\n b = 1\n \nend";
let model = model_of(src);
let offset = after(src, "b = 1\n ");
let names: Vec<String> = Resolver::new(&model, &lib)
.with_workspace(Some((ws, Vec::new())))
.visible(offset, Namespace::Value)
.into_iter()
.map(|c| c.name.to_string())
.collect();
assert!(names.contains(&"sibling".to_string()), "{names:?}");
let pos = |n: &str| names.iter().position(|x| x == n).unwrap();
assert!(pos("b") < pos("sibling"));
assert!(pos("sibling") < pos("greet"));
assert!(pos("greet") < pos("println"));
}
fn visible_names(
src: &str,
needle: &str,
lib: &BTreeMap<String, Arc<PackageIndex>>,
) -> Vec<String> {
let model = model_of(src);
let offset = after(src, needle);
Resolver::new(&model, lib)
.visible(offset, Namespace::Value)
.into_iter()
.map(|c| c.name.to_string())
.collect()
}
#[test]
fn visible_lists_all_tiers_in_masking_order() {
let lib = library(&[package("Base", &["println"]), package("A", &["greet"])]);
let src = "using A\nfunction f(a)\n b = 1\n \nend";
let names = visible_names(src, "b = 1\n ", &lib);
for expected in ["a", "b", "f", "greet", "println"] {
assert!(
names.contains(&expected.to_string()),
"missing {expected} in {names:?}"
);
}
assert!(names.iter().position(|n| n == "b") < names.iter().position(|n| n == "greet"));
assert!(
names.iter().position(|n| n == "greet") < names.iter().position(|n| n == "println")
);
}
#[test]
fn visible_drops_shadowed_names() {
let lib = library(&[package("Base", &["map"])]);
let src = "function f()\n map = 1\n \nend";
let names = visible_names(src, "map = 1\n ", &lib);
assert_eq!(names.iter().filter(|n| *n == "map").count(), 1);
}
#[test]
fn occurrence_keys_carry_the_dev_package() {
let src = "function f()\n f()\nend\n";
let model = model_of(src);
let lib = library(&[]);
let key_in = |pkg_name: &str| -> OccurrenceKey {
let ws = workspace_pkg(pkg_name, &["f"], &[]);
let keys: Vec<OccurrenceKey> = Resolver::new(&model, &lib)
.with_workspace(Some((ws, Vec::new())))
.workspace_occurrences()
.into_keys()
.collect();
assert_eq!(keys.len(), 1, "one bucket for `f` in {pkg_name}");
keys.into_iter().next().unwrap()
};
let a = key_in("PkgA");
let b = key_in("PkgB");
assert_eq!(a.package.as_str(), "PkgA");
assert_eq!(b.package.as_str(), "PkgB");
assert_eq!(
(a.module.clone(), a.namespace, a.name.clone()),
(b.module.clone(), b.namespace, b.name.clone())
);
assert_ne!(a, b, "same symbol shape, different package, distinct keys");
}
type OccMap = BTreeMap<(Namespace, SmolStr), Vec<(u32, u32, bool)>>;
fn workspace_occ(
src: &str,
workspace: &Arc<PackageIndex>,
lib: &BTreeMap<String, Arc<PackageIndex>>,
) -> OccMap {
let model = model_of(src);
Resolver::new(&model, lib)
.with_workspace(Some((Arc::clone(workspace), Vec::new())))
.workspace_occurrences()
.into_iter()
.map(|(key, recs)| {
let simple = recs
.iter()
.map(|r| (r.range.start().into(), r.range.end().into(), r.is_def))
.collect();
((key.namespace, key.name), simple)
})
.collect()
}
#[test]
fn defining_file_reports_its_module_global() {
let ws = workspace_pkg("MyPkg", &["f"], &[]);
let src = "function f()\n f()\nend\n";
let occ = workspace_occ(src, &ws, &library(&[]));
let recs = occ
.get(&(Namespace::Value, SmolStr::new("f")))
.expect("f is a workspace symbol");
assert_eq!(recs.len(), 2);
assert!(recs.iter().any(|r| r.2), "the definition site is present");
assert!(recs.iter().any(|r| !r.2), "the intra-file use is present");
}
#[test]
fn using_file_reports_free_reads_of_a_workspace_symbol() {
let ws = workspace_pkg("MyPkg", &["f"], &[]);
let src = "g() = f() + f()\n";
let occ = workspace_occ(src, &ws, &library(&[]));
let recs = occ
.get(&(Namespace::Value, SmolStr::new("f")))
.expect("f resolves to the workspace");
assert_eq!(recs.len(), 2, "both calls to f");
assert!(recs.iter().all(|r| !r.2), "uses, not definitions");
}
#[test]
fn a_shadowing_local_is_not_a_workspace_occurrence() {
let ws = workspace_pkg("MyPkg", &["f"], &[]);
let src = "function g()\n f = 1\n f + f\nend\n";
let occ = workspace_occ(src, &ws, &library(&[]));
assert!(
!occ.contains_key(&(Namespace::Value, SmolStr::new("f"))),
"the local f shadows the workspace symbol"
);
}
#[test]
fn non_member_file_reports_nothing() {
let model = model_of("f() = f()\n");
let lib = library(&[]);
let occ = Resolver::new(&model, &lib).workspace_occurrences();
assert!(occ.is_empty());
}
#[test]
fn macro_occurrences_use_the_macro_namespace() {
let ws = workspace_pkg("MyPkg", &[], &["@m"]);
let occ = workspace_occ("macro m()\nend\n", &ws, &library(&[]));
assert!(occ.contains_key(&(Namespace::Macro, SmolStr::new("@m"))));
let occ2 = workspace_occ("f() = @m\n", &ws, &library(&[]));
assert!(occ2.contains_key(&(Namespace::Macro, SmolStr::new("@m"))));
}
#[test]
fn a_plain_global_is_not_a_workspace_occurrence() {
let ws = workspace_pkg("MyPkg", &["f"], &[]);
let src = "x = 1\nf() = x\n";
let occ = workspace_occ(src, &ws, &library(&[]));
assert!(!occ.contains_key(&(Namespace::Value, SmolStr::new("x"))));
assert!(occ.contains_key(&(Namespace::Value, SmolStr::new("f"))));
}
}