pub mod relations;
pub use relations::ElixirGraphBuilder;
use sqry_core::ast::{Scope, ScopeId, link_nested_scopes};
use sqry_core::plugin::{
LanguageMetadata, LanguagePlugin,
error::{ParseError, ScopeError},
};
use std::path::Path;
use tree_sitter::{Language, Node, Parser, Tree};
const LANGUAGE_ID: &str = "elixir";
const LANGUAGE_NAME: &str = "Elixir";
const TREE_SITTER_VERSION: &str = "0.23";
pub struct ElixirPlugin {
graph_builder: ElixirGraphBuilder,
}
impl ElixirPlugin {
#[must_use]
pub fn new() -> Self {
Self {
graph_builder: ElixirGraphBuilder::default(),
}
}
}
impl Default for ElixirPlugin {
fn default() -> Self {
Self::new()
}
}
impl LanguagePlugin for ElixirPlugin {
fn metadata(&self) -> LanguageMetadata {
LanguageMetadata {
id: LANGUAGE_ID,
name: LANGUAGE_NAME,
version: env!("CARGO_PKG_VERSION"),
author: "Verivus Pty Ltd",
description: "Elixir language support for sqry",
tree_sitter_version: TREE_SITTER_VERSION,
}
}
fn extensions(&self) -> &'static [&'static str] {
&["ex", "exs"]
}
fn language(&self) -> Language {
tree_sitter_elixir_sqry::language()
}
fn parse_ast(&self, content: &[u8]) -> Result<Tree, ParseError> {
let mut parser = Parser::new();
parser
.set_language(&self.language())
.map_err(|e| ParseError::LanguageSetFailed(e.to_string()))?;
parser
.parse(content, None)
.ok_or(ParseError::TreeSitterFailed)
}
fn extract_scopes(
&self,
tree: &Tree,
content: &[u8],
file_path: &Path,
) -> Result<Vec<Scope>, ScopeError> {
Ok(Self::extract_elixir_scopes(tree, content, file_path))
}
fn graph_builder(&self) -> Option<&dyn sqry_core::graph::GraphBuilder> {
Some(&self.graph_builder)
}
}
impl ElixirPlugin {
fn extract_elixir_scopes(tree: &Tree, content: &[u8], file_path: &Path) -> Vec<Scope> {
let mut scopes = Vec::new();
Self::collect_scopes_from_node(tree.root_node(), content, file_path, &mut scopes);
scopes.sort_by_key(|s| (s.start_line, s.start_column));
link_nested_scopes(&mut scopes);
scopes
}
fn collect_scopes_from_node(
node: Node<'_>,
content: &[u8],
file_path: &Path,
scopes: &mut Vec<Scope>,
) {
if node.kind() == "call" {
let macro_name = node
.child_by_field_name("identifier")
.or_else(|| node.child_by_field_name("target"))
.and_then(|n| n.utf8_text(content).ok());
if let Some(name) = macro_name {
let (scope_type, scope_name) = match name {
"defmodule" | "defprotocol" | "defimpl" => {
let module_name = Self::extract_module_name_for_scope(node, content);
("module", module_name)
}
"def" | "defp" | "defmacro" | "defmacrop" => {
let func_name = Self::extract_function_name_for_scope(node, content);
("function", func_name)
}
_ => (name, None),
};
if matches!(scope_type, "module" | "function") {
let scope_name = scope_name.unwrap_or_else(|| "<anonymous>".to_string());
let start = node.start_position();
let end = node.end_position();
scopes.push(Scope {
id: ScopeId::new(0), scope_type: scope_type.to_string(),
name: scope_name,
file_path: file_path.to_path_buf(),
start_line: start.row + 1,
start_column: start.column,
end_line: end.row + 1,
end_column: end.column,
parent_id: None,
});
}
}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.is_named() {
Self::collect_scopes_from_node(child, content, file_path, scopes);
}
}
}
fn extract_module_name_for_scope(node: Node<'_>, content: &[u8]) -> Option<String> {
let arguments = node.child_by_field_name("arguments").or_else(|| {
let mut cursor = node.walk();
node.children(&mut cursor).find(|c| c.kind() == "arguments")
})?;
let mut cursor = arguments.walk();
arguments
.children(&mut cursor)
.find(|child| {
child.is_named() && matches!(child.kind(), "alias" | "identifier" | "atom")
})
.and_then(|child| child.utf8_text(content).ok())
.map(String::from)
}
fn extract_function_name_for_scope(node: Node<'_>, content: &[u8]) -> Option<String> {
let arguments = node.child_by_field_name("arguments").or_else(|| {
let mut cursor = node.walk();
node.children(&mut cursor).find(|c| c.kind() == "arguments")
})?;
let mut cursor = arguments.walk();
for child in arguments.children(&mut cursor) {
if !child.is_named() {
continue;
}
match child.kind() {
"call" => {
if let Some(target) = child.child_by_field_name("target") {
return target.utf8_text(content).ok().map(String::from);
}
let mut inner_cursor = child.walk();
for inner in child.children(&mut inner_cursor) {
if inner.is_named() && inner.kind() == "identifier" {
return inner.utf8_text(content).ok().map(String::from);
}
}
}
"identifier" => {
return child.utf8_text(content).ok().map(String::from);
}
"binary_operator" => {
if let Some(left) = child.child_by_field_name("left") {
if left.kind() == "call" {
if let Some(target) = left.child_by_field_name("target") {
return target.utf8_text(content).ok().map(String::from);
}
} else if left.kind() == "identifier" {
return left.utf8_text(content).ok().map(String::from);
}
}
}
_ => {}
}
}
None
}
}