Skip to main content

blast_radius/language/
mod.rs

1//! Language adapters.
2//!
3//! Each supported language is a single self-contained [`LanguageAdapter`]: it
4//! declares the file extensions it owns, parses source into the shared
5//! [`ModuleFacts`], and resolves its own import specifiers against the shared
6//! [`ResolveCtx`]. The registry below is the one place languages are enumerated
7//! — discovery (`fs`), parse dispatch (`parse`), and import resolution
8//! (`resolve`) all derive from it, so adding a language is a single new adapter
9//! plus one registry line.
10
11use std::path::Path;
12use std::sync::OnceLock;
13
14use anyhow::Result;
15
16use crate::parse::ModuleFacts;
17use crate::resolve::{Resolution, ResolveCtx};
18
19mod javascript;
20use javascript::JavaScriptAdapter;
21
22#[cfg(any(feature = "vue", feature = "svelte"))]
23mod component;
24#[cfg(feature = "svelte")]
25use component::SvelteAdapter;
26#[cfg(feature = "vue")]
27use component::VueAdapter;
28
29#[cfg(feature = "python")]
30mod python;
31#[cfg(feature = "python")]
32use python::PythonAdapter;
33
34#[cfg(feature = "rust")]
35mod rust_lang;
36#[cfg(feature = "rust")]
37use rust_lang::RustAdapter;
38
39/// A language's parsing and resolution behavior. Stateless: resolution reads
40/// shared indexes from the [`ResolveCtx`] passed in.
41pub(crate) trait LanguageAdapter: Send + Sync {
42    /// File extensions this adapter owns, in resolution-preference order (e.g.
43    /// `ts` before `js`). Used both for repo discovery and extension probing.
44    fn extensions(&self) -> &'static [&'static str];
45
46    /// Whether this adapter handles the given file path.
47    fn handles(&self, path: &Path) -> bool {
48        path.extension()
49            .and_then(|ext| ext.to_str())
50            .is_some_and(|ext| self.extensions().contains(&ext))
51    }
52
53    fn parse(&self, path: &Path, source: &str) -> Result<ModuleFacts>;
54
55    fn resolve(&self, ctx: &ResolveCtx, importer: &Path, specifier: &str) -> Resolution;
56
57    fn is_internal(&self, ctx: &ResolveCtx, importer: &Path, specifier: &str) -> bool;
58}
59
60fn registry() -> &'static [Box<dyn LanguageAdapter>] {
61    static REGISTRY: OnceLock<Vec<Box<dyn LanguageAdapter>>> = OnceLock::new();
62    REGISTRY.get_or_init(|| {
63        // JavaScript/TypeScript is always present and is the default fallback,
64        // so it must come first (its extensions also win resolution ties).
65        // `mut` is unused when no optional language features are enabled.
66        #[allow(unused_mut)]
67        let mut adapters: Vec<Box<dyn LanguageAdapter>> = vec![Box::new(JavaScriptAdapter)];
68        #[cfg(feature = "python")]
69        adapters.push(Box::new(PythonAdapter));
70        #[cfg(feature = "rust")]
71        adapters.push(Box::new(RustAdapter));
72        #[cfg(feature = "vue")]
73        adapters.push(Box::new(VueAdapter));
74        #[cfg(feature = "svelte")]
75        adapters.push(Box::new(SvelteAdapter));
76        adapters
77    })
78}
79
80/// The adapter that owns `path`, falling back to JavaScript/TypeScript for any
81/// extension no other adapter claims (matching historical parser behavior).
82pub(crate) fn adapter_for(path: &Path) -> &'static dyn LanguageAdapter {
83    let registry = registry();
84    registry
85        .iter()
86        .find(|adapter| adapter.handles(path))
87        .unwrap_or(&registry[0])
88        .as_ref()
89}
90
91/// Every source extension across the compiled-in adapters. Used by repo
92/// discovery to decide which files to index. Note this is the union across all
93/// languages; per-language resolution only probes its own family's extensions
94/// (see each adapter's resolution logic) so a Python import never resolves to a
95/// `.ts` file, and vice versa.
96fn source_extensions() -> &'static [&'static str] {
97    static EXTENSIONS: OnceLock<Vec<&'static str>> = OnceLock::new();
98    EXTENSIONS.get_or_init(|| {
99        registry()
100            .iter()
101            .flat_map(|adapter| adapter.extensions().iter().copied())
102            .collect()
103    })
104}
105
106/// Whether `ext` belongs to any compiled-in language (used by repo discovery).
107pub(crate) fn is_source_extension(ext: &str) -> bool {
108    source_extensions().contains(&ext)
109}