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