use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use rowan::ast::AstNode;
use crate::ast::{AstToken, CallExpr, Expr, HasArgList, Name, StringLiteral};
use crate::semantic::{ScopeKind, SemanticModel};
use crate::syntax::{SyntaxKind, SyntaxNode};
pub fn file_exports(model: &SemanticModel) -> BTreeSet<String> {
model
.bindings()
.iter()
.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
.free_reads()
.map(|ident| ident.name.to_string())
.collect()
}
pub fn file_qualified_reads(model: &SemanticModel) -> BTreeSet<String> {
model
.qualified_reads()
.iter()
.map(|read| {
read.path
.iter()
.map(|component| component.as_str())
.collect::<Vec<_>>()
.join(".")
})
.collect()
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct IncludeEdge {
pub raw: String,
pub target: Option<PathBuf>,
pub host_suffix: Vec<String>,
}
pub fn include_edges(root: &SyntaxNode, base_dir: Option<&Path>) -> Vec<IncludeEdge> {
root.descendants()
.filter_map(CallExpr::cast)
.filter_map(|call| {
let raw = include_target(&call)?;
let target = resolve_target(&raw, base_dir);
let host_suffix = enclosing_module_names(call.syntax());
Some(IncludeEdge {
raw,
target,
host_suffix,
})
})
.collect()
}
fn enclosing_module_names(node: &SyntaxNode) -> Vec<String> {
let mut names: Vec<String> = node
.ancestors()
.filter(|ancestor| ancestor.kind() == SyntaxKind::MODULE_DEF)
.filter_map(|module| module_def_name(&module))
.collect();
names.reverse();
names
}
fn module_def_name(module: &SyntaxNode) -> Option<String> {
let signature = module
.children()
.find(|child| child.kind() == SyntaxKind::SIGNATURE)?;
let name = signature
.children()
.find(|child| child.kind() == SyntaxKind::NAME)?;
Some(Name::cast(name)?.ident()?.text().to_string())
}
pub fn include_call_sites(root: &SyntaxNode) -> Vec<(String, rowan::TextRange)> {
root.descendants()
.filter_map(CallExpr::cast)
.filter_map(|call| Some((include_target(&call)?, call.syntax().text_range())))
.collect()
}
pub(crate) fn include_target(call: &CallExpr) -> Option<String> {
Some(
include_literal(call)?
.content_tokens()
.map(|token| token.text().to_string())
.collect(),
)
}
pub(crate) fn include_literal(call: &CallExpr) -> Option<StringLiteral> {
let Expr::Name(callee) = call.callee()? else {
return None;
};
if callee.ident()?.text() != "include" {
return None;
}
let mut args = call.arg_list()?.args();
let arg = args.next()?;
if args.next().is_some() {
return None;
}
let Expr::StringLiteral(string) = arg.expr()? else {
return None;
};
if string.prefix().is_some() || string.interpolations().next().is_some() {
return None;
}
Some(string)
}
pub(crate) fn resolve_target(raw: &str, base_dir: Option<&Path>) -> Option<PathBuf> {
let path = Path::new(raw);
if path.is_absolute() {
Some(path.to_path_buf())
} else {
base_dir.map(|dir| dir.join(path))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::parse;
fn model_of(src: &str) -> SemanticModel {
SemanticModel::build(&parse(src).cst)
}
fn names(set: &BTreeSet<String>) -> Vec<&str> {
set.iter().map(String::as_str).collect()
}
fn edges_of(src: &str, base_dir: Option<&Path>) -> Vec<IncludeEdge> {
include_edges(&parse(src).cst, base_dir)
}
#[test]
fn exports_are_top_level_bindings_including_imports() {
let m = model_of("f() = 1\nx = 2\nimport A\n");
assert_eq!(names(&file_exports(&m)), ["A", "f", "x"]);
}
#[test]
fn exports_exclude_params_and_function_locals() {
let m = model_of("function g(a)\n t = a\n t\nend\n");
assert_eq!(names(&file_exports(&m)), ["g"]);
}
#[test]
fn exports_exclude_module_interior() {
let m = model_of("module M\ny = 1\nend\n");
assert_eq!(names(&file_exports(&m)), ["M"]);
}
#[test]
fn free_reads_are_the_unbound_names() {
let m = model_of("f() = 1\ny = sin(x)\n");
assert_eq!(names(&file_free_reads(&m)), ["sin", "x"]);
}
#[test]
fn qualified_reads_join_the_full_dotted_path() {
let m = model_of("a.b.c\nBase.@time f()\n");
assert_eq!(names(&file_qualified_reads(&m)), ["Base.@time", "a.b.c"]);
}
#[test]
fn collects_static_includes_in_source_order() {
let edges = edges_of("include(\"a.jl\")\ninclude(\"sub/b.jl\")\n", None);
let raws: Vec<_> = edges.iter().map(|edge| edge.raw.as_str()).collect();
assert_eq!(raws, ["a.jl", "sub/b.jl"]);
assert!(edges.iter().all(|edge| edge.target.is_none()));
}
#[test]
fn resolves_relative_include_against_base_dir() {
let edges = edges_of("include(\"sub/b.jl\")\n", Some(Path::new("/proj/src")));
assert_eq!(edges[0].target, Some(PathBuf::from("/proj/src/sub/b.jl")));
}
#[test]
fn absolute_include_ignores_base_dir() {
let edges = edges_of("include(\"/etc/a.jl\")\n", Some(Path::new("/proj")));
assert_eq!(edges[0].target, Some(PathBuf::from("/etc/a.jl")));
}
#[test]
fn host_suffix_is_empty_at_file_top_level() {
let edges = edges_of("include(\"a.jl\")\n", None);
assert!(edges[0].host_suffix.is_empty());
}
#[test]
fn host_suffix_records_single_enclosing_module() {
let edges = edges_of("module A\ninclude(\"a.jl\")\nend\n", None);
assert_eq!(edges[0].host_suffix, ["A"]);
}
#[test]
fn host_suffix_records_nested_modules_outermost_first() {
let edges = edges_of("module A\nmodule B\ninclude(\"a.jl\")\nend\nend\n", None);
assert_eq!(edges[0].host_suffix, ["A", "B"]);
}
#[test]
fn skips_dynamic_interpolated_qualified_and_two_arg_includes() {
let edges = edges_of(
"include(x)\ninclude(\"$d/a.jl\")\nM.include(\"a.jl\")\ninclude(f, \"a.jl\")\n",
None,
);
assert!(edges.is_empty(), "only static bare `include(\"…\")` counts");
}
}