Skip to main content

lanekeep_lang/
lib.rs

1//! Language trait and registry for lanekeep.
2//!
3//! The `Language` trait and the registry mapping file extensions onto grammars.
4//!
5//! This abstraction exists before it has a second implementor on purpose. Retrofitting it
6//! after a second language arrives is the expensive version of the same work.
7
8pub mod binding;
9
10use std::collections::BTreeMap;
11use std::fmt;
12use std::path::Path;
13use std::sync::Arc;
14
15use thiserror::Error;
16
17/// A language's stable identifier, as written in a rule's `language` field.
18///
19/// Deliberately not an enum. An enum would have to live in this crate and name every
20/// language, so adding one would mean editing the abstraction rather than adding an
21/// implementor — exactly the coupling the trait exists to avoid.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
23pub struct LanguageId(&'static str);
24
25impl LanguageId {
26    /// Declare an identifier. Called by language implementations.
27    #[must_use]
28    pub const fn new(id: &'static str) -> Self {
29        Self(id)
30    }
31
32    /// The identifier as it appears in configuration.
33    #[must_use]
34    pub const fn as_str(self) -> &'static str {
35        self.0
36    }
37}
38
39impl fmt::Display for LanguageId {
40    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41        f.write_str(self.0)
42    }
43}
44
45/// A language lanekeep can parse.
46///
47/// `Send + Sync` because the walker runs files across rayon workers, and every worker needs
48/// the grammar.
49///
50/// Binding resolution — the light semantic layer behind the import-resolution host
51/// functions — is deliberately not a method here yet. Its signature depends on the tree
52/// and file context types, which do not exist. Adding a method to a trait with no external
53/// implementors is cheap; committing to a half-designed signature is not.
54pub trait Language: Send + Sync {
55    /// Stable identifier, as written in a rule's `language` field.
56    fn id(&self) -> LanguageId;
57
58    /// Identifier resolution for this language, when it has any.
59    ///
60    /// Returns `None` for a language with no resolver yet, which is honest rather than a
61    /// placeholder: a rule asking about bindings in such a language gets nothing back
62    /// instead of a confidently wrong answer.
63    fn resolver(&self) -> Option<Arc<dyn binding::BindingResolver>> {
64        None
65    }
66
67    /// Extensions this language claims, without the leading dot, lowercase.
68    ///
69    /// Two languages must not claim the same extension; the registry rejects that at
70    /// registration rather than picking a winner.
71    fn extensions(&self) -> &'static [&'static str];
72
73    /// The tree-sitter grammar.
74    fn grammar(&self) -> tree_sitter::Language;
75
76    /// The grammar's ABI version.
77    ///
78    /// This is a cache key input. A grammar bump changes node shapes and therefore query
79    /// results, so an entry computed under a different ABI is not a valid entry — it would
80    /// serve results derived from a tree that no longer exists.
81    ///
82    /// Read from the grammar rather than written down, or it stops tracking the thing it
83    /// exists to track the first time someone forgets to update it. Note that bundled
84    /// grammars do not share an ABI — TypeScript and JavaScript currently differ — which
85    /// is why this is per-language rather than one global constant.
86    fn grammar_abi(&self) -> usize {
87        self.grammar().abi_version()
88    }
89}
90
91/// Why a language could not be registered.
92#[derive(Debug, Clone, PartialEq, Eq, Error)]
93pub enum RegistryError {
94    /// Two languages claim the same identifier.
95    #[error("language `{0}` is already registered")]
96    DuplicateId(String),
97
98    /// Two languages claim the same file extension.
99    #[error(
100        "extension `.{extension}` is claimed by both `{existing}` and `{incoming}`: \
101         a file cannot belong to two languages"
102    )]
103    DuplicateExtension {
104        /// The contested extension.
105        extension: String,
106        /// The language that claimed it first.
107        existing: String,
108        /// The language that tried to claim it second.
109        incoming: String,
110    },
111
112    /// A language declared an extension that cannot match anything.
113    #[error("language `{language}` declared invalid extension `{extension}`: {reason}")]
114    InvalidExtension {
115        /// The language at fault.
116        language: String,
117        /// The extension as declared.
118        extension: String,
119        /// What is wrong with it.
120        reason: &'static str,
121    },
122}
123
124/// Which languages are available, and which files belong to them.
125#[derive(Clone, Default)]
126pub struct LanguageRegistry {
127    by_id: BTreeMap<&'static str, Arc<dyn Language>>,
128    by_extension: BTreeMap<&'static str, Arc<dyn Language>>,
129}
130
131/// Hand-written because `Arc<dyn Language>` is not `Debug` — trait objects would have to
132/// require it, which is a demand on every implementor for the sake of one impl here. The
133/// keys are the useful part anyway: what is registered, and what it claims.
134impl fmt::Debug for LanguageRegistry {
135    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136        f.debug_struct("LanguageRegistry")
137            .field("by_id", &self.by_id.keys().collect::<Vec<_>>())
138            .field(
139                "by_extension",
140                &self.by_extension.keys().collect::<Vec<_>>(),
141            )
142            .finish()
143    }
144}
145
146impl LanguageRegistry {
147    /// An empty registry.
148    #[must_use]
149    pub fn new() -> Self {
150        Self::default()
151    }
152
153    /// Add a language.
154    ///
155    /// # Errors
156    ///
157    /// Fails when the identifier or any extension is already claimed, or when an extension
158    /// is malformed. Rejecting rather than overwriting is deliberate: a registry that
159    /// silently let the last registration win would make which language parses a `.ts`
160    /// file depend on registration order, and that order is not part of any contract.
161    ///
162    /// A failed registration changes nothing — validation of every extension completes
163    /// before any is claimed.
164    pub fn register(&mut self, language: Arc<dyn Language>) -> Result<(), RegistryError> {
165        let id = language.id().as_str();
166
167        if self.by_id.contains_key(id) {
168            return Err(RegistryError::DuplicateId(id.to_owned()));
169        }
170
171        for extension in language.extensions() {
172            let invalid = |reason: &'static str| RegistryError::InvalidExtension {
173                language: id.to_owned(),
174                extension: (*extension).to_owned(),
175                reason,
176            };
177
178            if extension.is_empty() {
179                return Err(invalid("must not be empty"));
180            }
181            if extension.starts_with('.') {
182                return Err(invalid("must not include the leading dot"));
183            }
184            if extension.chars().any(|c| c.is_ascii_uppercase()) {
185                return Err(invalid(
186                    "must be lowercase; lookup lowercases the path's extension",
187                ));
188            }
189            if let Some(existing) = self.by_extension.get(extension) {
190                return Err(RegistryError::DuplicateExtension {
191                    extension: (*extension).to_owned(),
192                    existing: existing.id().as_str().to_owned(),
193                    incoming: id.to_owned(),
194                });
195            }
196        }
197
198        for extension in language.extensions() {
199            self.by_extension.insert(extension, Arc::clone(&language));
200        }
201        self.by_id.insert(id, language);
202        Ok(())
203    }
204
205    /// Look up a language by its identifier.
206    #[must_use]
207    pub fn by_id(&self, id: &str) -> Option<&Arc<dyn Language>> {
208        self.by_id.get(id)
209    }
210
211    /// Which language, if any, parses this path.
212    ///
213    /// The extension is lowercased before lookup, so a file named `Button.TSX` is still
214    /// TSX. Without this, whether a file gets checked would depend on how it was typed.
215    #[must_use]
216    pub fn for_path(&self, path: impl AsRef<Path>) -> Option<&Arc<dyn Language>> {
217        let extension = path.as_ref().extension()?.to_str()?.to_ascii_lowercase();
218        self.by_extension.get(extension.as_str())
219    }
220
221    /// Every registered language, ordered by identifier.
222    pub fn languages(&self) -> impl Iterator<Item = &Arc<dyn Language>> {
223        self.by_id.values()
224    }
225
226    /// Every extension any language claims, ordered.
227    pub fn extensions(&self) -> impl Iterator<Item = &'static str> + '_ {
228        self.by_extension.keys().copied()
229    }
230
231    /// How many languages are registered.
232    #[must_use]
233    pub fn len(&self) -> usize {
234        self.by_id.len()
235    }
236
237    /// Whether no language is registered.
238    #[must_use]
239    pub fn is_empty(&self) -> bool {
240        self.by_id.is_empty()
241    }
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247
248    /// A stand-in whose grammar is never exercised. The registry's job is bookkeeping, and
249    /// testing it through a real language would couple these tests to whichever one exists.
250    struct Fake {
251        id: LanguageId,
252        extensions: &'static [&'static str],
253    }
254
255    impl Language for Fake {
256        fn id(&self) -> LanguageId {
257            self.id
258        }
259        fn extensions(&self) -> &'static [&'static str] {
260            self.extensions
261        }
262        fn grammar(&self) -> tree_sitter::Language {
263            unreachable!("registry tests never touch the grammar")
264        }
265        fn grammar_abi(&self) -> usize {
266            0
267        }
268    }
269
270    fn fake(id: &'static str, extensions: &'static [&'static str]) -> Arc<dyn Language> {
271        Arc::new(Fake {
272            id: LanguageId::new(id),
273            extensions,
274        })
275    }
276
277    fn registry(languages: &[Arc<dyn Language>]) -> LanguageRegistry {
278        let mut registry = LanguageRegistry::new();
279        for language in languages {
280            registry.register(Arc::clone(language)).expect("registers");
281        }
282        registry
283    }
284
285    #[test]
286    fn finds_a_language_by_id() {
287        let registry = registry(&[fake("alpha", &["a"])]);
288        assert_eq!(
289            registry.by_id("alpha").expect("present").id().as_str(),
290            "alpha"
291        );
292        assert!(registry.by_id("missing").is_none());
293    }
294
295    #[test]
296    fn finds_a_language_by_path() {
297        let registry = registry(&[fake("alpha", &["a", "aa"]), fake("beta", &["b"])]);
298
299        assert_eq!(
300            registry.for_path("src/x.a").expect("matches").id().as_str(),
301            "alpha"
302        );
303        assert_eq!(
304            registry
305                .for_path("src/x.aa")
306                .expect("matches")
307                .id()
308                .as_str(),
309            "alpha"
310        );
311        assert_eq!(
312            registry.for_path("src/x.b").expect("matches").id().as_str(),
313            "beta"
314        );
315        assert!(registry.for_path("src/x.zzz").is_none());
316        assert!(registry.for_path("src/noextension").is_none());
317    }
318
319    #[test]
320    fn extension_lookup_ignores_case() {
321        // A case-insensitive filesystem lets `Button.TSX` and `Button.tsx` name the same
322        // file. Whether it gets checked must not depend on how it was typed.
323        let registry = registry(&[fake("alpha", &["a"])]);
324        assert!(registry.for_path("src/x.A").is_some());
325        assert!(registry.for_path("src/x.a").is_some());
326    }
327
328    #[test]
329    fn rejects_a_duplicate_id() {
330        let mut registry = registry(&[fake("alpha", &["a"])]);
331        let err = registry
332            .register(fake("alpha", &["z"]))
333            .expect_err("duplicate id");
334        assert_eq!(err, RegistryError::DuplicateId("alpha".to_owned()));
335    }
336
337    #[test]
338    fn rejects_a_contested_extension() {
339        // The important one. Letting the last registration win would make which language
340        // parses a file depend on registration order — an order nothing guarantees, and a
341        // difference that would surface as results changing for no visible reason.
342        let mut registry = registry(&[fake("alpha", &["a"])]);
343        let err = registry
344            .register(fake("beta", &["a"]))
345            .expect_err("contested extension");
346
347        match err {
348            RegistryError::DuplicateExtension {
349                extension,
350                existing,
351                incoming,
352            } => {
353                assert_eq!(extension, "a");
354                assert_eq!(existing, "alpha");
355                assert_eq!(incoming, "beta");
356            }
357            other => panic!("wrong error: {other:?}"),
358        }
359    }
360
361    #[test]
362    fn a_rejected_registration_leaves_no_trace() {
363        // Partial registration would be worse than rejection: the language would be
364        // unreachable by id while still owning whichever extensions were processed before
365        // the conflict.
366        let mut registry = registry(&[fake("alpha", &["a"])]);
367        let _ = registry.register(fake("beta", &["b", "a", "c"]));
368
369        assert!(registry.by_id("beta").is_none());
370        assert!(
371            registry.for_path("x.b").is_none(),
372            "b must not have been claimed"
373        );
374        assert!(
375            registry.for_path("x.c").is_none(),
376            "c must not have been claimed"
377        );
378        assert_eq!(
379            registry.for_path("x.a").expect("still alpha").id().as_str(),
380            "alpha"
381        );
382        assert_eq!(registry.len(), 1);
383    }
384
385    #[test]
386    fn rejects_malformed_extensions() {
387        let mut registry = LanguageRegistry::new();
388
389        // A leading dot would never match, because `Path::extension` strips it.
390        assert!(matches!(
391            registry.register(fake("dotted", &[".a"])),
392            Err(RegistryError::InvalidExtension { .. })
393        ));
394        // Uppercase would never match either, since lookup lowercases first.
395        assert!(matches!(
396            registry.register(fake("shouty", &["A"])),
397            Err(RegistryError::InvalidExtension { .. })
398        ));
399        assert!(matches!(
400            registry.register(fake("empty", &[""])),
401            Err(RegistryError::InvalidExtension { .. })
402        ));
403        assert!(registry.is_empty());
404    }
405
406    #[test]
407    fn iteration_order_is_stable() {
408        // Anything derived from registry order — a `--help` listing, an error naming the
409        // valid languages — must not reorder between runs.
410        let registry = registry(&[
411            fake("zeta", &["z"]),
412            fake("alpha", &["a"]),
413            fake("mu", &["m"]),
414        ]);
415
416        let ids: Vec<&str> = registry.languages().map(|l| l.id().as_str()).collect();
417        assert_eq!(ids, ["alpha", "mu", "zeta"]);
418        assert_eq!(registry.extensions().collect::<Vec<_>>(), ["a", "m", "z"]);
419    }
420
421    #[test]
422    fn an_empty_registry_matches_nothing() {
423        let registry = LanguageRegistry::new();
424        assert!(registry.is_empty());
425        assert_eq!(registry.len(), 0);
426        assert!(registry.for_path("src/x.ts").is_none());
427        assert!(registry.by_id("typescript").is_none());
428    }
429}