pub mod binding;
use std::collections::BTreeMap;
use std::fmt;
use std::path::Path;
use std::sync::Arc;
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct LanguageId(&'static str);
impl LanguageId {
#[must_use]
pub const fn new(id: &'static str) -> Self {
Self(id)
}
#[must_use]
pub const fn as_str(self) -> &'static str {
self.0
}
}
impl fmt::Display for LanguageId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.0)
}
}
pub trait Language: Send + Sync {
fn id(&self) -> LanguageId;
fn resolver(&self) -> Option<Arc<dyn binding::BindingResolver>> {
None
}
fn extensions(&self) -> &'static [&'static str];
fn grammar(&self) -> tree_sitter::Language;
fn grammar_abi(&self) -> usize {
self.grammar().abi_version()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum RegistryError {
#[error("language `{0}` is already registered")]
DuplicateId(String),
#[error(
"extension `.{extension}` is claimed by both `{existing}` and `{incoming}`: \
a file cannot belong to two languages"
)]
DuplicateExtension {
extension: String,
existing: String,
incoming: String,
},
#[error("language `{language}` declared invalid extension `{extension}`: {reason}")]
InvalidExtension {
language: String,
extension: String,
reason: &'static str,
},
}
#[derive(Clone, Default)]
pub struct LanguageRegistry {
by_id: BTreeMap<&'static str, Arc<dyn Language>>,
by_extension: BTreeMap<&'static str, Arc<dyn Language>>,
}
impl fmt::Debug for LanguageRegistry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("LanguageRegistry")
.field("by_id", &self.by_id.keys().collect::<Vec<_>>())
.field(
"by_extension",
&self.by_extension.keys().collect::<Vec<_>>(),
)
.finish()
}
}
impl LanguageRegistry {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn register(&mut self, language: Arc<dyn Language>) -> Result<(), RegistryError> {
let id = language.id().as_str();
if self.by_id.contains_key(id) {
return Err(RegistryError::DuplicateId(id.to_owned()));
}
for extension in language.extensions() {
let invalid = |reason: &'static str| RegistryError::InvalidExtension {
language: id.to_owned(),
extension: (*extension).to_owned(),
reason,
};
if extension.is_empty() {
return Err(invalid("must not be empty"));
}
if extension.starts_with('.') {
return Err(invalid("must not include the leading dot"));
}
if extension.chars().any(|c| c.is_ascii_uppercase()) {
return Err(invalid(
"must be lowercase; lookup lowercases the path's extension",
));
}
if let Some(existing) = self.by_extension.get(extension) {
return Err(RegistryError::DuplicateExtension {
extension: (*extension).to_owned(),
existing: existing.id().as_str().to_owned(),
incoming: id.to_owned(),
});
}
}
for extension in language.extensions() {
self.by_extension.insert(extension, Arc::clone(&language));
}
self.by_id.insert(id, language);
Ok(())
}
#[must_use]
pub fn by_id(&self, id: &str) -> Option<&Arc<dyn Language>> {
self.by_id.get(id)
}
#[must_use]
pub fn for_path(&self, path: impl AsRef<Path>) -> Option<&Arc<dyn Language>> {
let extension = path.as_ref().extension()?.to_str()?.to_ascii_lowercase();
self.by_extension.get(extension.as_str())
}
pub fn languages(&self) -> impl Iterator<Item = &Arc<dyn Language>> {
self.by_id.values()
}
pub fn extensions(&self) -> impl Iterator<Item = &'static str> + '_ {
self.by_extension.keys().copied()
}
#[must_use]
pub fn len(&self) -> usize {
self.by_id.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.by_id.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
struct Fake {
id: LanguageId,
extensions: &'static [&'static str],
}
impl Language for Fake {
fn id(&self) -> LanguageId {
self.id
}
fn extensions(&self) -> &'static [&'static str] {
self.extensions
}
fn grammar(&self) -> tree_sitter::Language {
unreachable!("registry tests never touch the grammar")
}
fn grammar_abi(&self) -> usize {
0
}
}
fn fake(id: &'static str, extensions: &'static [&'static str]) -> Arc<dyn Language> {
Arc::new(Fake {
id: LanguageId::new(id),
extensions,
})
}
fn registry(languages: &[Arc<dyn Language>]) -> LanguageRegistry {
let mut registry = LanguageRegistry::new();
for language in languages {
registry.register(Arc::clone(language)).expect("registers");
}
registry
}
#[test]
fn finds_a_language_by_id() {
let registry = registry(&[fake("alpha", &["a"])]);
assert_eq!(
registry.by_id("alpha").expect("present").id().as_str(),
"alpha"
);
assert!(registry.by_id("missing").is_none());
}
#[test]
fn finds_a_language_by_path() {
let registry = registry(&[fake("alpha", &["a", "aa"]), fake("beta", &["b"])]);
assert_eq!(
registry.for_path("src/x.a").expect("matches").id().as_str(),
"alpha"
);
assert_eq!(
registry
.for_path("src/x.aa")
.expect("matches")
.id()
.as_str(),
"alpha"
);
assert_eq!(
registry.for_path("src/x.b").expect("matches").id().as_str(),
"beta"
);
assert!(registry.for_path("src/x.zzz").is_none());
assert!(registry.for_path("src/noextension").is_none());
}
#[test]
fn extension_lookup_ignores_case() {
let registry = registry(&[fake("alpha", &["a"])]);
assert!(registry.for_path("src/x.A").is_some());
assert!(registry.for_path("src/x.a").is_some());
}
#[test]
fn rejects_a_duplicate_id() {
let mut registry = registry(&[fake("alpha", &["a"])]);
let err = registry
.register(fake("alpha", &["z"]))
.expect_err("duplicate id");
assert_eq!(err, RegistryError::DuplicateId("alpha".to_owned()));
}
#[test]
fn rejects_a_contested_extension() {
let mut registry = registry(&[fake("alpha", &["a"])]);
let err = registry
.register(fake("beta", &["a"]))
.expect_err("contested extension");
match err {
RegistryError::DuplicateExtension {
extension,
existing,
incoming,
} => {
assert_eq!(extension, "a");
assert_eq!(existing, "alpha");
assert_eq!(incoming, "beta");
}
other => panic!("wrong error: {other:?}"),
}
}
#[test]
fn a_rejected_registration_leaves_no_trace() {
let mut registry = registry(&[fake("alpha", &["a"])]);
let _ = registry.register(fake("beta", &["b", "a", "c"]));
assert!(registry.by_id("beta").is_none());
assert!(
registry.for_path("x.b").is_none(),
"b must not have been claimed"
);
assert!(
registry.for_path("x.c").is_none(),
"c must not have been claimed"
);
assert_eq!(
registry.for_path("x.a").expect("still alpha").id().as_str(),
"alpha"
);
assert_eq!(registry.len(), 1);
}
#[test]
fn rejects_malformed_extensions() {
let mut registry = LanguageRegistry::new();
assert!(matches!(
registry.register(fake("dotted", &[".a"])),
Err(RegistryError::InvalidExtension { .. })
));
assert!(matches!(
registry.register(fake("shouty", &["A"])),
Err(RegistryError::InvalidExtension { .. })
));
assert!(matches!(
registry.register(fake("empty", &[""])),
Err(RegistryError::InvalidExtension { .. })
));
assert!(registry.is_empty());
}
#[test]
fn iteration_order_is_stable() {
let registry = registry(&[
fake("zeta", &["z"]),
fake("alpha", &["a"]),
fake("mu", &["m"]),
]);
let ids: Vec<&str> = registry.languages().map(|l| l.id().as_str()).collect();
assert_eq!(ids, ["alpha", "mu", "zeta"]);
assert_eq!(registry.extensions().collect::<Vec<_>>(), ["a", "m", "z"]);
}
#[test]
fn an_empty_registry_matches_nothing() {
let registry = LanguageRegistry::new();
assert!(registry.is_empty());
assert_eq!(registry.len(), 0);
assert!(registry.for_path("src/x.ts").is_none());
assert!(registry.by_id("typescript").is_none());
}
}