use super::{get_language, resolution_utils, CallTarget, TypeRelationKind};
use crate::grep::language_from_extension;
use crate::indexer::code_region_extractor::{extract_meaningful_regions, CodeRegion};
use crate::indexer::file_utils::FileUtils;
use crate::indexer::signature_extractor::extract_signatures;
use std::path::Path;
use tree_sitter::{Node, Parser, Tree};
const SOURCE: &str = r#"
defmodule MyApp.Accounts do
alias MyApp.{Repo, User}
import MyApp.Validation
require Logger
use MyApp.Telemetry
defstruct [:name, :email]
def fetch_user(id) when is_integer(id) do
Repo.get(User, id)
end
defp validate(user), do: MyApp.Validation.call(user)
defmacro active(query) do
quote(do: where(unquote(query), [u], u.active))
end
end
defprotocol MyApp.Renderable do
def render(value)
end
defimpl MyApp.Renderable, for: MyApp.User do
def render(user), do: user.name
end
"#;
fn parse(source: &str) -> Tree {
let language = get_language("elixir").expect("Elixir language should be registered");
let mut parser = Parser::new();
parser
.set_language(&language.get_ts_language())
.expect("Elixir grammar should load");
parser
.parse(source, None)
.expect("Elixir source should parse")
}
fn walk_calls<F>(node: Node, visit: &mut F)
where
F: FnMut(Node),
{
if node.kind() == "call" || node.kind() == "binary_operator" {
visit(node);
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
walk_calls(child, visit);
}
}
#[test]
fn registers_and_detects_elixir_extensions() {
let language = get_language("elixir").expect("Elixir language should be available");
assert_eq!(language.name(), "elixir");
assert_eq!(language.get_file_extensions(), vec!["ex", "exs"]);
for file in ["lib/accounts.ex", "mix.exs", "test/accounts_test.exs"] {
assert_eq!(FileUtils::detect_language(Path::new(file)), Some("elixir"));
assert_eq!(
resolution_utils::detect_language_from_path(file).as_deref(),
Some("elixir")
);
assert_eq!(language_from_extension(Path::new(file)), Some("elixir"));
}
}
#[test]
fn parses_realistic_modules_without_errors() {
let tree = parse(SOURCE);
assert!(!tree.root_node().has_error(), "{:#?}", tree.root_node());
}
#[test]
fn chunks_declarations_without_chunking_ordinary_calls_or_whole_modules() {
let language = get_language("elixir").unwrap();
let tree = parse(SOURCE);
let mut regions: Vec<CodeRegion> = Vec::new();
extract_meaningful_regions(tree.root_node(), SOURCE, language.as_ref(), &mut regions);
assert_eq!(
regions.len(),
3,
"regions: {:?}",
regions.iter().map(|r| &r.content).collect::<Vec<_>>()
);
assert!(regions
.iter()
.any(|region| region.content.contains("def fetch_user")));
assert!(regions
.iter()
.any(|region| region.content.starts_with("defmacro active")));
assert!(regions
.iter()
.any(|region| region.content.contains("defstruct")));
assert!(!regions
.iter()
.any(|region| region.content.starts_with("defmodule")));
assert!(!regions
.iter()
.any(|region| region.content.starts_with("Repo.get")));
}
#[test]
fn extracts_semantic_signatures_and_declaration_kinds() {
let language = get_language("elixir").unwrap();
let tree = parse(SOURCE);
let signatures = extract_signatures(tree.root_node(), SOURCE, language.as_ref());
let summary: Vec<_> = signatures
.iter()
.map(|signature| (signature.kind.as_str(), signature.name.as_str()))
.collect();
assert!(summary.contains(&("module", "MyApp.Accounts")));
assert!(summary.contains(&("struct", "MyApp.Accounts")));
assert!(summary.contains(&("function", "fetch_user")));
assert!(summary.contains(&("function", "validate")));
assert!(summary.contains(&("macro", "active")));
assert!(summary.contains(&("interface", "MyApp.Renderable")));
assert!(summary.contains(&("implementation", "MyApp.Renderable for MyApp.User")));
assert_eq!(
summary.iter().filter(|(_, name)| *name == "render").count(),
2
);
}
#[test]
fn extracts_imports_exports_calls_owners_and_protocol_relations() {
let language = get_language("elixir").unwrap();
let tree = parse(SOURCE);
let mut imports = Vec::new();
let mut exports = Vec::new();
let mut calls: Vec<CallTarget> = Vec::new();
let mut declarations = Vec::new();
let mut relations = Vec::new();
walk_calls(tree.root_node(), &mut |node| {
let (node_imports, node_exports) = language.extract_imports_exports(node, SOURCE);
imports.extend(node_imports);
exports.extend(node_exports);
calls.extend(language.extract_function_calls(node, SOURCE));
if let Some(name) = language.extract_declaration_name(node, SOURCE) {
declarations.push((name, language.extract_symbol_owner(node, SOURCE)));
}
for (kind, target) in language.extract_type_relations(node, SOURCE) {
relations.push((
kind,
target,
language.extract_type_relation_source(node, SOURCE),
));
}
});
imports.sort();
imports.dedup();
assert_eq!(
imports,
vec![
"Logger",
"MyApp.Repo",
"MyApp.Telemetry",
"MyApp.User",
"MyApp.Validation"
]
);
assert!(exports.contains(&"MyApp.Accounts".to_string()));
assert!(exports.contains(&"fetch_user".to_string()));
assert!(!exports.contains(&"validate".to_string()));
assert!(calls.contains(&CallTarget {
name: "get".to_string(),
qualifier: Some("Repo".to_string())
}));
assert!(calls.contains(&CallTarget {
name: "call".to_string(),
qualifier: Some("MyApp::Validation".to_string())
}));
assert!(!calls
.iter()
.any(|call| call.name == "fetch_user" || call.name == "def"));
assert!(declarations.contains(&("fetch_user".to_string(), Some("MyApp.Accounts".to_string()))));
assert_eq!(
relations,
vec![(
TypeRelationKind::Implements,
"MyApp.Renderable".to_string(),
Some("MyApp.User".to_string())
)]
);
}
#[test]
fn resolves_mix_module_paths() {
let language = get_language("elixir").unwrap();
let files = vec![
"lib/my_app/accounts.ex".to_string(),
"lib/my_app/repo.ex".to_string(),
"lib/my_app/user.ex".to_string(),
"test/support/my_app/user.exs".to_string(),
];
let files = resolution_utils::FileRegistry::new(&files);
assert_eq!(
language.resolve_import("MyApp.Repo", "lib/my_app/accounts.ex", &files),
Some("lib/my_app/repo.ex".to_string())
);
assert_eq!(
language.resolve_import("MyApp.User", "lib/my_app/accounts.ex", &files),
Some("lib/my_app/user.ex".to_string())
);
assert_eq!(
language.resolve_import("External.Package", "lib/my_app/accounts.ex", &files),
None
);
}
#[test]
fn describe_block_splits_into_individual_test_regions() {
let source = r#"
defmodule MyApp.AccountsTest do
use ExUnit.Case
alias MyApp.Accounts
describe "fetch_user/1" do
test "returns the user when the id exists in the database" do
user = Accounts.fetch_user(42)
assert user.id == 42
assert user.name == "Ada Lovelace"
end
test "returns nil when the id is not present in the database" do
user = Accounts.fetch_user(999)
assert is_nil(user)
end
end
end
"#;
let language = get_language("elixir").unwrap();
let tree = parse(source);
let mut regions = Vec::new();
extract_meaningful_regions(tree.root_node(), source, language.as_ref(), &mut regions);
let test_regions: Vec<_> = regions
.iter()
.filter(|region| region.content.trim_start().starts_with("test \""))
.collect();
assert_eq!(
test_regions.len(),
2,
"expected 2 separate regions for the nested test blocks, got {} (regions: {:?})",
test_regions.len(),
regions.iter().map(|r| &r.content).collect::<Vec<_>>()
);
let describe_regions: Vec<_> = regions
.iter()
.filter(|region| region.content.trim_start().starts_with("describe "))
.collect();
assert!(
describe_regions.is_empty(),
"describe block should not collapse into a single blob region, got: {:?}",
describe_regions
.iter()
.map(|r| &r.content)
.collect::<Vec<_>>()
);
}
#[test]
fn indexes_exunit_tests_as_owned_function_like_declarations() {
let source = r#"
defmodule MyApp.AccountsTest do
use ExUnit.Case
alias MyApp.Accounts
test "fetches an account" do
assert Accounts.fetch_user(42)
end
end
"#;
let language = get_language("elixir").unwrap();
let tree = parse(source);
let mut regions = Vec::new();
extract_meaningful_regions(tree.root_node(), source, language.as_ref(), &mut regions);
assert_eq!(regions.len(), 1);
assert!(regions[0]
.content
.starts_with("test \"fetches an account\""));
let signatures = extract_signatures(tree.root_node(), source, language.as_ref());
assert!(signatures.iter().any(|signature| {
signature.kind == "function" && signature.name == "fetches an account"
}));
let mut declaration = None;
walk_calls(tree.root_node(), &mut |node| {
if language.extract_declaration_name(node, source).as_deref() == Some("fetches an account")
{
declaration = Some((
language.extract_declaration_kind(node, source),
language.extract_symbol_owner(node, source),
));
}
});
assert_eq!(
declaration,
Some((Some("function"), Some("MyApp.AccountsTest".to_string())))
);
}