use std::collections::{BTreeMap, BTreeSet};
use rowan::NodeOrToken;
use rowan::ast::AstNode as _;
use crate::ast::{AssignmentExpr, FunctionExpr};
use crate::semantic::{BindingKind, ScopeKind, SemanticModel};
use crate::syntax::SyntaxNode;
pub fn file_exports(model: &SemanticModel) -> BTreeSet<String> {
model
.bindings()
.iter()
.filter(|binding| matches!(binding.kind, BindingKind::Local | BindingKind::Implicit))
.filter(|binding| model.scope(binding.scope).kind == ScopeKind::File)
.map(|binding| binding.name.to_string())
.collect()
}
pub fn file_free_reads(model: &SemanticModel) -> BTreeSet<String> {
model
.idents()
.iter()
.filter(|ident| model.resolve_local(ident).is_none())
.map(|ident| ident.name.to_string())
.collect()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, salsa::Update)]
pub enum DefKind {
Function,
Value,
}
pub fn file_def_sites(model: &SemanticModel, root: &SyntaxNode) -> BTreeMap<String, DefKind> {
let mut defs: BTreeMap<String, DefKind> = BTreeMap::new();
for binding in model.bindings() {
if !matches!(binding.kind, BindingKind::Local | BindingKind::Implicit) {
continue;
}
if model.scope(binding.scope).kind != ScopeKind::File {
continue;
}
let name = binding.name.to_string();
match classify_def(root, binding.def_range) {
DefKind::Function => {
defs.insert(name, DefKind::Function);
}
DefKind::Value => {
defs.entry(name).or_insert(DefKind::Value);
}
}
}
defs
}
fn classify_def(root: &SyntaxNode, def_range: rowan::TextRange) -> DefKind {
let element = root.covering_element(def_range);
let start = match element {
NodeOrToken::Node(node) => node,
NodeOrToken::Token(token) => match token.parent() {
Some(parent) => parent,
None => return DefKind::Value,
},
};
for ancestor in start.ancestors() {
if let Some(assign) = AssignmentExpr::cast(ancestor) {
return match assign.value_element() {
Some(NodeOrToken::Node(value)) if FunctionExpr::can_cast(value.kind()) => {
DefKind::Function
}
_ => DefKind::Value,
};
}
}
DefKind::Value
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::parse;
fn exports_of(src: &str) -> BTreeSet<String> {
file_exports(&SemanticModel::build(&parse(src).cst))
}
fn names(set: &BTreeSet<String>) -> Vec<&str> {
set.iter().map(String::as_str).collect()
}
fn def_sites_of(src: &str) -> BTreeMap<String, DefKind> {
let cst = parse(src).cst;
file_def_sites(&SemanticModel::build(&cst), &cst)
}
#[test]
fn collects_top_level_assignments() {
let e = exports_of("x <- 1\ny <- function() 2\nz = 3\n");
assert_eq!(names(&e), vec!["x", "y", "z"]);
}
#[test]
fn excludes_function_locals_and_params() {
let e = exports_of("f <- function(a) {\n b <- a + 1\n b\n}\n");
assert_eq!(names(&e), vec!["f"]);
}
#[test]
fn includes_top_level_super_assignment() {
let e = exports_of("g <<- 1\n");
assert_eq!(names(&e), vec!["g"]);
}
#[test]
fn free_reads_exclude_locally_resolved_names() {
let model = SemanticModel::build(&parse("x <- 1\nfoo(x, y)\n").cst);
let reads = file_free_reads(&model);
assert_eq!(names(&reads), vec!["foo", "y"]);
}
#[test]
fn def_sites_tag_functions_and_values() {
let d = def_sites_of("x <- 1\nf <- function() 2\ng <- \\(a) a\nh = function(b) b\n");
assert_eq!(d.get("x"), Some(&DefKind::Value));
assert_eq!(d.get("f"), Some(&DefKind::Function));
assert_eq!(d.get("g"), Some(&DefKind::Function));
assert_eq!(d.get("h"), Some(&DefKind::Function));
}
#[test]
fn def_sites_match_export_names_and_exclude_locals() {
let src = "f <- function(a) {\n b <- function() a\n b\n}\ny <<- 3\n";
let d = def_sites_of(src);
let mut names: Vec<&str> = d.keys().map(String::as_str).collect();
names.sort();
assert_eq!(names, vec!["f", "y"]);
assert_eq!(d.get("f"), Some(&DefKind::Function));
assert_eq!(d.get("y"), Some(&DefKind::Value));
}
#[test]
fn def_sites_function_wins_over_value_on_rebind() {
let d = def_sites_of("p <- 1\np <- function() 2\n");
assert_eq!(d.get("p"), Some(&DefKind::Function));
let d = def_sites_of("q <- function() 2\nq <- 1\n");
assert_eq!(d.get("q"), Some(&DefKind::Function));
}
}