use serde::{Deserialize, Serialize};
pub const NO_CONSTRUCTOR_METHOD_NAMES: &[&str] = &[];
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CapabilityLevel {
Exact,
Partial,
Unsupported,
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
pub enum CallableDeclarationFamily {
#[default]
None,
SameSignature,
FunctionClauses,
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
pub enum CallTextPrefilter {
#[default]
Disabled,
Parenthesized,
ParenthesizedOrCommand,
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
pub struct ModulePathSyntax {
pub rooted_prefixes: &'static [&'static str],
pub repeatable_rooted_prefixes: &'static [&'static str],
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
pub struct ReceiverTypeSyntax {
pub wrapper_calls: &'static [&'static str],
pub class_object_suffixes: &'static [&'static str],
}
impl ReceiverTypeSyntax {
#[must_use]
pub const fn none() -> Self {
Self {
wrapper_calls: &[],
class_object_suffixes: &[],
}
}
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
pub struct CallableReferenceSyntax {
pub prefixes: &'static [&'static str],
pub numeric_arity_suffix: bool,
pub symbol_wrapper: Option<&'static str>,
pub trailing_invocation_punctuation: bool,
}
impl CallableReferenceSyntax {
#[must_use]
pub const fn none() -> Self {
Self {
prefixes: &[],
numeric_arity_suffix: false,
symbol_wrapper: None,
trailing_invocation_punctuation: false,
}
}
}
impl ModulePathSyntax {
#[must_use]
pub const fn none() -> Self {
Self {
rooted_prefixes: &[],
repeatable_rooted_prefixes: &[],
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct LanguageCapabilities {
pub modules: CapabilityLevel,
pub generics: CapabilityLevel,
pub macros: CapabilityLevel,
pub dynamic_dispatch: CapabilityLevel,
pub exceptions: CapabilityLevel,
pub async_await: CapabilityLevel,
pub coroutines: CapabilityLevel,
pub reflection: CapabilityLevel,
pub ffi: CapabilityLevel,
pub pattern_matching: CapabilityLevel,
pub receiver_types: CapabilityLevel,
pub field_places_complete: bool,
pub module_export_aliases: &'static [&'static str],
pub module_default_export_names: &'static [&'static str],
pub universal_type_names: &'static [&'static str],
pub module_path_syntax: ModulePathSyntax,
pub constructor_method_names: &'static [&'static str],
pub bare_call_constructor_syntax: bool,
pub super_receiver_tokens: &'static [&'static str],
pub implicit_receiver_tokens: &'static [&'static str],
pub receiver_type_syntax: ReceiverTypeSyntax,
pub same_directory_unqualified_calls: bool,
pub build_target_linkage: bool,
pub callable_declaration_family: CallableDeclarationFamily,
pub quoted_callable_literals: bool,
pub callable_reference_syntax: CallableReferenceSyntax,
pub call_text_prefilter: CallTextPrefilter,
pub module_resolution_extensions: &'static [&'static str],
pub workspace_manifest_context_extensions: &'static [&'static str],
}
impl LanguageCapabilities {
#[must_use]
pub fn effective_constructor_method_names(&self) -> &'static [&'static str] {
self.constructor_method_names
}
#[must_use]
pub fn effective_super_receiver_tokens(&self) -> &'static [&'static str] {
self.super_receiver_tokens
}
#[must_use]
pub fn effective_implicit_receiver_tokens(&self) -> &'static [&'static str] {
self.implicit_receiver_tokens
}
#[must_use]
pub const fn unsupported() -> Self {
Self {
modules: CapabilityLevel::Unsupported,
generics: CapabilityLevel::Unsupported,
macros: CapabilityLevel::Unsupported,
dynamic_dispatch: CapabilityLevel::Unsupported,
exceptions: CapabilityLevel::Unsupported,
async_await: CapabilityLevel::Unsupported,
coroutines: CapabilityLevel::Unsupported,
reflection: CapabilityLevel::Unsupported,
ffi: CapabilityLevel::Unsupported,
pattern_matching: CapabilityLevel::Unsupported,
receiver_types: CapabilityLevel::Unsupported,
field_places_complete: false,
module_export_aliases: &[],
module_default_export_names: &[],
universal_type_names: &[],
module_path_syntax: ModulePathSyntax::none(),
constructor_method_names: &[],
bare_call_constructor_syntax: false,
super_receiver_tokens: &[],
implicit_receiver_tokens: &[],
receiver_type_syntax: ReceiverTypeSyntax::none(),
same_directory_unqualified_calls: false,
build_target_linkage: false,
callable_declaration_family: CallableDeclarationFamily::None,
quoted_callable_literals: false,
callable_reference_syntax: CallableReferenceSyntax::none(),
call_text_prefilter: CallTextPrefilter::Disabled,
module_resolution_extensions: &[],
workspace_manifest_context_extensions: &[],
}
}
#[must_use]
pub const fn partial_baseline() -> Self {
Self {
modules: CapabilityLevel::Partial,
generics: CapabilityLevel::Partial,
macros: CapabilityLevel::Unsupported,
dynamic_dispatch: CapabilityLevel::Partial,
exceptions: CapabilityLevel::Partial,
async_await: CapabilityLevel::Partial,
coroutines: CapabilityLevel::Partial,
reflection: CapabilityLevel::Unsupported,
ffi: CapabilityLevel::Unsupported,
pattern_matching: CapabilityLevel::Partial,
receiver_types: CapabilityLevel::Unsupported,
field_places_complete: false,
module_export_aliases: &[],
module_default_export_names: &[],
universal_type_names: &[],
module_path_syntax: ModulePathSyntax::none(),
constructor_method_names: &[],
bare_call_constructor_syntax: false,
super_receiver_tokens: &[],
implicit_receiver_tokens: &[],
receiver_type_syntax: ReceiverTypeSyntax::none(),
same_directory_unqualified_calls: false,
build_target_linkage: false,
callable_declaration_family: CallableDeclarationFamily::None,
quoted_callable_literals: false,
callable_reference_syntax: CallableReferenceSyntax::none(),
call_text_prefilter: CallTextPrefilter::Disabled,
module_resolution_extensions: &[],
workspace_manifest_context_extensions: &[],
}
}
}
#[must_use]
pub fn callable_reference_variants(
raw: &str,
syntax: CallableReferenceSyntax,
quoted_callable_literals: bool,
) -> Vec<String> {
let mut out = Vec::new();
push_callable_variant(&mut out, raw);
let value = raw.trim();
if value.is_empty() {
return out;
}
if quoted_callable_literals {
if let Some(inner) = quoted_bare_callable(value) {
push_callable_variant(&mut out, inner);
}
}
for prefix in syntax.prefixes {
if let Some(rest) = value.strip_prefix(prefix) {
push_callable_variant(
&mut out,
if syntax.numeric_arity_suffix {
strip_numeric_arity(rest.trim())
} else {
rest.trim()
},
);
}
}
if let Some(wrapper) = syntax.symbol_wrapper {
if let Some(inner) = value
.strip_prefix(wrapper)
.and_then(|rest| rest.trim().strip_prefix('('))
.and_then(|rest| rest.strip_suffix(')'))
{
let inner = bonsai_common::trim_leading_name_punctuation(inner.trim());
if looks_like_callable_ident(inner) {
push_callable_variant(&mut out, inner);
}
}
}
if syntax.trailing_invocation_punctuation {
let trimmed = value
.trim_end_matches(|ch: char| !ch.is_alphanumeric() && ch != '_')
.trim();
push_callable_variant(&mut out, trimmed);
}
let has_numeric_arity = value
.rsplit_once('/')
.is_some_and(|(_, arity)| !arity.is_empty() && arity.chars().all(|ch| ch.is_ascii_digit()));
let tail_source = if syntax.numeric_arity_suffix {
strip_numeric_arity(value)
} else {
value
};
let tail = bonsai_common::short_qualified_tail(tail_source);
if !tail_source.chars().any(char::is_whitespace)
&& (!has_numeric_arity || syntax.numeric_arity_suffix)
&& tail != tail_source
{
push_callable_variant(
&mut out,
if syntax.numeric_arity_suffix {
strip_numeric_arity(tail)
} else {
tail
},
);
}
out
}
fn push_callable_variant(out: &mut Vec<String>, value: &str) {
let value = value.trim();
if !value.is_empty() && !out.iter().any(|existing| existing == value) {
out.push(value.to_string());
}
}
fn strip_numeric_arity(value: &str) -> &str {
let value = value.trim();
value
.rsplit_once('/')
.filter(|(name, arity)| !name.is_empty() && arity.chars().all(|ch| ch.is_ascii_digit()))
.map_or(value, |(name, _)| name.trim())
}
fn looks_like_callable_ident(value: &str) -> bool {
let mut chars = value.chars();
matches!(chars.next(), Some(ch) if ch == '_' || ch.is_alphabetic())
&& chars.all(|ch| ch == '_' || ch.is_alphanumeric())
}
fn quoted_bare_callable(value: &str) -> Option<&str> {
let value = value.trim();
let quote = value.as_bytes().first().copied()?;
if !matches!(quote, b'\'' | b'"') || value.as_bytes().last().copied()? != quote {
return None;
}
let inner = value.get(1..value.len().saturating_sub(1))?.trim();
looks_like_callable_ident(inner).then_some(inner)
}
#[cfg(test)]
mod tests {
use super::{callable_reference_variants, CallableReferenceSyntax, LanguageCapabilities};
#[test]
fn empty_receiver_capabilities_mean_no_receiver_syntax() {
let capabilities = LanguageCapabilities::unsupported();
assert!(capabilities.effective_super_receiver_tokens().is_empty());
assert!(capabilities.effective_implicit_receiver_tokens().is_empty());
}
#[test]
fn callable_reference_transformations_require_adapter_declarations() {
let plain = LanguageCapabilities::unsupported();
assert_eq!(
callable_reference_variants("keyword run/2", plain.callable_reference_syntax, false),
vec!["keyword run/2"]
);
let syntax = CallableReferenceSyntax {
prefixes: &["keyword "],
numeric_arity_suffix: true,
symbol_wrapper: Some("symbol"),
trailing_invocation_punctuation: true,
};
assert!(callable_reference_variants("keyword run/2", syntax, false).contains(&"run".to_string()));
assert!(callable_reference_variants("symbol(:run)", syntax, false).contains(&"run".to_string()));
assert!(callable_reference_variants("run.", syntax, false).contains(&"run".to_string()));
assert!(callable_reference_variants("'run'", syntax, true).contains(&"run".to_string()));
}
}