use crate::parsing::LanguageBehavior;
use crate::parsing::ResolutionScope;
use crate::parsing::behavior_state::{BehaviorState, StatefulBehavior};
use crate::{FileId, Visibility};
use std::path::{Path, PathBuf};
use tree_sitter::Language;
#[derive(Clone)]
pub struct ClojureBehavior {
language: Language,
state: BehaviorState,
}
impl ClojureBehavior {
pub fn new() -> Self {
Self {
language: tree_sitter_clojure_orchard::LANGUAGE.into(),
state: BehaviorState::new(),
}
}
}
impl StatefulBehavior for ClojureBehavior {
fn state(&self) -> &BehaviorState {
&self.state
}
}
impl Default for ClojureBehavior {
fn default() -> Self {
Self::new()
}
}
impl LanguageBehavior for ClojureBehavior {
fn language_id(&self) -> crate::parsing::registry::LanguageId {
crate::parsing::registry::LanguageId::new("clojure")
}
fn format_path_as_module(&self, components: &[&str]) -> Option<String> {
if components.is_empty() {
None
} else {
Some(components.join(".").replace('_', "-"))
}
}
fn configure_symbol(&self, symbol: &mut crate::Symbol, module_path: Option<&str>) {
if let Some(path) = module_path {
symbol.module_path = Some(path.to_string().into());
}
if let Some(ref sig) = symbol.signature {
symbol.visibility = self.parse_visibility(sig);
}
}
fn create_resolution_context(&self, file_id: FileId) -> Box<dyn ResolutionScope> {
Box::new(crate::parsing::clojure::ClojureResolutionContext::new(
file_id,
))
}
fn create_inheritance_resolver(&self) -> Box<dyn crate::parsing::InheritanceResolver> {
Box::new(crate::parsing::GenericInheritanceResolver::new())
}
fn format_module_path(&self, base_path: &str, _symbol_name: &str) -> String {
base_path.to_string()
}
fn parse_visibility(&self, signature: &str) -> Visibility {
if signature.contains("defn-")
|| signature.contains("^:private")
|| signature.contains("^{:private true}")
{
Visibility::Private
} else {
Visibility::Public
}
}
fn module_separator(&self) -> &'static str {
"." }
fn supports_traits(&self) -> bool {
true }
fn supports_inherent_methods(&self) -> bool {
false }
fn get_language(&self) -> Language {
self.language.clone()
}
fn module_path_from_file(
&self,
file_path: &Path,
project_root: &Path,
_extensions: &[&str],
) -> Option<String> {
let relative = file_path.strip_prefix(project_root).ok()?;
let path_str = relative.to_string_lossy();
let without_src = path_str
.strip_prefix("src/")
.or_else(|| path_str.strip_prefix("src\\"))
.unwrap_or(&path_str);
let without_ext = without_src
.strip_suffix(".clj")
.or_else(|| without_src.strip_suffix(".cljc"))
.or_else(|| without_src.strip_suffix(".cljs"))
.or_else(|| without_src.strip_suffix(".edn"))
.unwrap_or(without_src);
let module_path = without_ext.replace(['/', '\\'], ".").replace('_', "-");
if module_path.is_empty() {
None
} else {
Some(module_path)
}
}
fn register_file(&self, path: PathBuf, file_id: FileId, module_path: String) {
self.register_file_with_state(path, file_id, module_path);
}
fn add_import(&self, import: crate::parsing::Import) {
self.add_import_with_state(import);
}
fn get_imports_for_file(&self, file_id: FileId) -> Vec<crate::parsing::Import> {
self.get_imports_from_state(file_id)
}
fn get_module_path_for_file(&self, file_id: FileId) -> Option<String> {
self.state.get_module_path(file_id)
}
fn import_matches_symbol(
&self,
import_path: &str,
symbol_module_path: &str,
_importing_module: Option<&str>,
) -> bool {
if import_path == symbol_module_path {
return true;
}
if symbol_module_path.starts_with(&format!("{import_path}.")) {
return true;
}
if let Some(last_dot) = import_path.rfind('.') {
let ns_part = &import_path[..last_dot];
if ns_part == symbol_module_path {
return true;
}
}
false
}
fn is_resolvable_symbol(&self, symbol: &crate::Symbol) -> bool {
use crate::SymbolKind;
matches!(
symbol.kind,
SymbolKind::Function
| SymbolKind::Variable
| SymbolKind::Macro
| SymbolKind::Interface
| SymbolKind::Struct
| SymbolKind::Method
| SymbolKind::Module
)
}
fn is_symbol_visible_from_file(&self, symbol: &crate::Symbol, from_file: FileId) -> bool {
if symbol.file_id == from_file {
return true;
}
match symbol.visibility {
Visibility::Public => true,
Visibility::Private => false,
Visibility::Module => {
if let Some(symbol_module) = &symbol.module_path {
if let Some(from_module) = self.get_module_path_for_file(from_file) {
return symbol_module.as_ref() == from_module;
}
}
false
}
_ => true,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_module_path() {
let behavior = ClojureBehavior::new();
assert_eq!(
behavior.format_module_path("my.namespace.core", "my-fn"),
"my.namespace.core"
);
}
#[test]
fn test_parse_visibility() {
let behavior = ClojureBehavior::new();
assert_eq!(
behavior.parse_visibility("(defn my-fn [x] ...)"),
Visibility::Public
);
assert_eq!(
behavior.parse_visibility("(defn- private-fn [x] ...)"),
Visibility::Private
);
assert_eq!(
behavior.parse_visibility("(def ^:private secret 42)"),
Visibility::Private
);
}
#[test]
fn test_module_separator() {
let behavior = ClojureBehavior::new();
assert_eq!(behavior.module_separator(), ".");
}
#[test]
fn test_supports_features() {
let behavior = ClojureBehavior::new();
assert!(behavior.supports_traits()); assert!(!behavior.supports_inherent_methods());
}
#[test]
fn test_module_path_from_file() {
let behavior = ClojureBehavior::new();
let root = Path::new("/project");
let exts = &["clj", "cljs", "cljc", "edn"];
let module_path = Path::new("/project/src/my/namespace/core.clj");
assert_eq!(
behavior.module_path_from_file(module_path, root, exts),
Some("my.namespace.core".to_string())
);
let underscore_path = Path::new("/project/src/my_app/some_module.clj");
assert_eq!(
behavior.module_path_from_file(underscore_path, root, exts),
Some("my-app.some-module".to_string())
);
let cljs_path = Path::new("/project/src/app/main.cljs");
assert_eq!(
behavior.module_path_from_file(cljs_path, root, exts),
Some("app.main".to_string())
);
}
}