cairo_lang_filesystem/
db.rs

1use std::collections::BTreeMap;
2use std::fs;
3use std::path::PathBuf;
4use std::sync::Arc;
5
6use cairo_lang_utils::Intern;
7use cairo_lang_utils::ordered_hash_map::OrderedHashMap;
8use salsa::{Database, Setter};
9use semver::Version;
10use serde::{Deserialize, Serialize};
11use smol_str::SmolStr;
12
13use crate::cfg::CfgSet;
14use crate::flag::Flag;
15use crate::ids::{
16    ArcStr, BlobId, BlobLongId, CodeMapping, CodeOrigin, CrateId, CrateInput, CrateLongId,
17    Directory, DirectoryInput, FileId, FileInput, FileLongId, FlagId, FlagLongId, SmolStrId,
18    SpanInFile, Tracked, VirtualFile,
19};
20use crate::span::{FileSummary, TextOffset, TextSpan, TextWidth};
21
22#[cfg(test)]
23#[path = "db_test.rs"]
24mod test;
25
26pub const CORELIB_CRATE_NAME: &str = "core";
27pub const CORELIB_VERSION: &str = env!("CARGO_PKG_VERSION");
28
29/// Unique identifier of a crate.
30///
31/// This directly translates to [DependencySettings::discriminator] except the discriminator
32/// **must** be `None` for the core crate.
33#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, Hash)]
34pub struct CrateIdentifier(String);
35
36impl<T: ToString> From<T> for CrateIdentifier {
37    fn from(value: T) -> Self {
38        Self(value.to_string())
39    }
40}
41
42impl From<CrateIdentifier> for String {
43    fn from(value: CrateIdentifier) -> Self {
44        value.0
45    }
46}
47
48/// Same as `CrateConfiguration` but without interning the root directory.
49/// This is used to avoid the need to intern the file id inside salsa database inputs.
50#[derive(Clone, Debug, PartialEq, Eq)]
51pub struct CrateConfigurationInput {
52    pub root: DirectoryInput,
53    pub settings: CrateSettings,
54    pub cache_file: Option<BlobLongId>,
55}
56
57impl CrateConfigurationInput {
58    /// Converts the input into an [`CrateConfiguration`].
59    pub fn into_crate_configuration(self, db: &dyn Database) -> CrateConfiguration<'_> {
60        CrateConfiguration {
61            root: self.root.into_directory(db),
62            settings: self.settings,
63            cache_file: self.cache_file.map(|blob_long_id| blob_long_id.intern(db)),
64        }
65    }
66}
67
68/// A configuration per crate.
69#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)]
70pub struct CrateConfiguration<'db> {
71    /// The root directory of the crate.
72    pub root: Directory<'db>,
73    pub settings: CrateSettings,
74    pub cache_file: Option<BlobId<'db>>,
75}
76impl<'db> CrateConfiguration<'db> {
77    /// Returns a new configuration.
78    pub fn default_for_root(root: Directory<'db>) -> Self {
79        Self { root, settings: CrateSettings::default(), cache_file: None }
80    }
81
82    /// Converts the configuration into an [`CrateConfigurationInput`].
83    pub fn into_crate_configuration_input(self, db: &dyn Database) -> CrateConfigurationInput {
84        CrateConfigurationInput {
85            root: self.root.into_directory_input(db),
86            settings: self.settings,
87            cache_file: self.cache_file.map(|blob_id| blob_id.long(db).clone()),
88        }
89    }
90}
91
92/// Same as `CrateConfiguration` but without the root directory.
93#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
94pub struct CrateSettings {
95    /// The name reflecting how the crate is referred to in the Cairo code e.g. `use crate_name::`.
96    /// If set to [`None`] then [`CrateIdentifier`] key will be used as a name.
97    pub name: Option<String>,
98    /// The crate's Cairo edition.
99    pub edition: Edition,
100    /// The crate's version.
101    ///
102    /// ## [CrateSettings::version] vs. [DependencySettings::discriminator]
103    ///
104    /// Cairo uses semantic versioning for crates.
105    /// The version field is an optional piece of metadata that can be attached to a crate
106    /// and is used in various lints and can be used as a context in diagnostics.
107    ///
108    /// On the other hand, the discriminator is a unique identifier that allows including multiple
109    /// copies of a crate in a single compilation unit.
110    /// It is free-form and never reaches the user.
111    pub version: Option<Version>,
112    /// The `#[cfg(...)]` configuration.
113    pub cfg_set: Option<CfgSet>,
114    /// The crate's dependencies.
115    #[serde(default)]
116    pub dependencies: BTreeMap<String, DependencySettings>,
117
118    #[serde(default)]
119    pub experimental_features: ExperimentalFeaturesConfig,
120}
121
122/// Tracked function to return the default settings for a crate.
123/// This is used to initialize the default settings once, and return it by reference.
124#[salsa::tracked(returns(ref))]
125pub fn default_crate_settings<'db>(_db: &'db dyn Database) -> CrateSettings {
126    CrateSettings::default()
127}
128
129/// The Cairo edition of a crate.
130///
131/// Editions are a mechanism to allow breaking changes in the compiler.
132/// Compiler minor version updates will always support all editions supported by the previous
133/// updates with the same major version. Compiler major version updates may remove support for older
134/// editions. Editions may be added to provide features that are not backwards compatible, while
135/// allowing user to opt-in to them, and be ready for later compiler updates.
136#[derive(
137    Clone, Copy, Debug, Default, Hash, PartialEq, Eq, Serialize, Deserialize, salsa::Update,
138)]
139pub enum Edition {
140    /// The base edition, dated for the first release of the compiler.
141    #[default]
142    #[serde(rename = "2023_01")]
143    V2023_01,
144    #[serde(rename = "2023_10")]
145    V2023_10,
146    #[serde(rename = "2023_11")]
147    V2023_11,
148    #[serde(rename = "2024_07")]
149    V2024_07,
150    #[serde(rename = "2025_12")]
151    V2025_12,
152}
153impl Edition {
154    /// Returns the latest stable edition.
155    ///
156    /// This Cairo edition is recommended for use in new projects and, in case of existing projects,
157    /// to migrate to when possible.
158    pub const fn latest() -> Self {
159        Self::V2025_12
160    }
161
162    /// The name of the prelude submodule of `core::prelude` for this compatibility version.
163    pub fn prelude_submodule_name<'db>(&self, db: &'db dyn Database) -> SmolStrId<'db> {
164        SmolStrId::from(
165            db,
166            match self {
167                Self::V2023_01 => "v2023_01",
168                Self::V2023_10 | Self::V2023_11 => "v2023_10",
169                Self::V2024_07 | Self::V2025_12 => "v2024_07",
170            },
171        )
172    }
173
174    /// Whether to ignore visibility modifiers.
175    pub fn ignore_visibility(&self) -> bool {
176        match self {
177            Self::V2023_01 | Self::V2023_10 => true,
178            Self::V2023_11 | Self::V2024_07 | Self::V2025_12 => false,
179        }
180    }
181
182    /// Whether to member access have the original type of the member.
183    pub fn member_access_desnaps(&self) -> bool {
184        match self {
185            Self::V2023_01 | Self::V2023_10 | Self::V2023_11 | Self::V2024_07 => false,
186            Self::V2025_12 => true,
187        }
188    }
189}
190
191/// The settings for a dependency.
192#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
193pub struct DependencySettings {
194    /// A unique string allowing identifying different copies of the same dependency
195    /// in the compilation unit.
196    ///
197    /// Usually such copies differ by their versions or sources (or both).
198    /// It **must** be [`None`] for the core crate, for other crates it should be directly
199    /// translated from their [`CrateIdentifier`].
200    pub discriminator: Option<String>,
201}
202
203/// Configuration per crate.
204#[derive(Clone, Debug, Default, Hash, PartialEq, Eq, Serialize, Deserialize)]
205pub struct ExperimentalFeaturesConfig {
206    pub negative_impls: bool,
207    /// Allows using associated item constraints.
208    pub associated_item_constraints: bool,
209    /// Allows using coupon types and coupon calls.
210    ///
211    /// Each function has an associated `Coupon` type, which represents paying the cost of the
212    /// function before calling it.
213    #[serde(default)]
214    pub coupons: bool,
215    /// Allows using user defined inline macros.
216    #[serde(default)]
217    pub user_defined_inline_macros: bool,
218    /// Allows using representation pointer types (&T), which desugar to BoxTrait<@T>.
219    #[serde(default)]
220    pub repr_ptrs: bool,
221}
222
223/// Function to get a virtual file from an external id.
224pub type ExtAsVirtual =
225    Arc<dyn for<'a> Fn(&'a dyn Database, salsa::Id) -> &'a VirtualFile<'a> + Send + Sync>;
226
227#[salsa::input]
228// TODO(eytan-starkware): Change this mechanism to hold input handles on the db struct outside
229// salsa mechanism, and invalidate manually.
230pub struct FilesGroupInput {
231    /// Main input of the project. Lists all the crates configurations.
232    #[returns(ref)]
233    pub crate_configs: Option<OrderedHashMap<CrateInput, CrateConfigurationInput>>,
234    /// Overrides for file content. Mostly used by language server and tests.
235    #[returns(ref)]
236    pub file_overrides: Option<OrderedHashMap<FileInput, Arc<str>>>,
237    // TODO(yuval): consider moving this to a separate crate, or rename this crate.
238    /// The compilation flags.
239    #[returns(ref)]
240    pub flags: Option<OrderedHashMap<FlagLongId, Arc<Flag>>>,
241    /// The `#[cfg(...)]` options.
242    #[returns(ref)]
243    pub cfg_set: Option<CfgSet>,
244    #[returns(ref)]
245    pub ext_as_virtual_obj: Option<ExtAsVirtual>,
246}
247
248#[salsa::tracked]
249pub fn files_group_input(db: &dyn Database) -> FilesGroupInput {
250    FilesGroupInput::new(db, None, None, None, None, None)
251}
252
253/// Queries over the files group.
254pub trait FilesGroup: Database {
255    /// Interned version of `crate_configs_input`.
256    fn crate_configs<'db>(&'db self) -> &'db OrderedHashMap<CrateId<'db>, CrateConfiguration<'db>> {
257        crate_configs(self.as_dyn_database())
258    }
259
260    /// Interned version of `file_overrides_input`.
261    fn file_overrides<'db>(&'db self) -> &'db OrderedHashMap<FileId<'db>, ArcStr> {
262        file_overrides(self.as_dyn_database())
263    }
264
265    /// Interned version of `flags_input`.
266    fn flags<'db>(&'db self) -> &'db OrderedHashMap<FlagId<'db>, Arc<Flag>> {
267        flags(self.as_dyn_database())
268    }
269
270    /// List of crates in the project.
271    fn crates<'db>(&'db self) -> &'db [CrateId<'db>] {
272        crates(self.as_dyn_database())
273    }
274
275    /// Configuration of the crate.
276    fn crate_config<'db>(
277        &'db self,
278        crate_id: CrateId<'db>,
279    ) -> Option<&'db CrateConfiguration<'db>> {
280        crate_config(self.as_dyn_database(), crate_id)
281    }
282
283    /// Query for the file contents. This takes overrides into consideration.
284    fn file_content<'db>(&'db self, file_id: FileId<'db>) -> Option<&'db str> {
285        file_content(self.as_dyn_database(), file_id).as_ref().map(|content| content.as_ref())
286    }
287
288    fn file_summary<'db>(&'db self, file_id: FileId<'db>) -> Option<&'db FileSummary> {
289        file_summary(self.as_dyn_database(), file_id)
290    }
291
292    /// Query for the blob content.
293    fn blob_content<'db>(&'db self, blob_id: BlobId<'db>) -> Option<&'db [u8]> {
294        blob_content(self.as_dyn_database(), blob_id)
295    }
296    /// Query to get a compilation flag by its ID.
297    fn get_flag<'db>(&'db self, id: FlagId<'db>) -> Option<&'db Flag> {
298        get_flag(self.as_dyn_database(), id)
299    }
300
301    /// Create an input file from an interned file id.
302    fn file_input<'db>(&'db self, file_id: FileId<'db>) -> &'db FileInput {
303        file_input(self.as_dyn_database(), file_id)
304    }
305
306    /// Create an input crate from an interned crate id.
307    fn crate_input<'db>(&'db self, crt: CrateId<'db>) -> &'db CrateInput {
308        crate_input(self.as_dyn_database(), crt)
309    }
310
311    /// Sets the given flag value. None value removes the flag.
312    fn set_flag(&mut self, flag: FlagLongId, value: Option<Arc<Flag>>) {
313        let db_ref = self.as_dyn_database();
314        let mut flags = files_group_input(db_ref).flags(db_ref).clone().unwrap();
315        match value {
316            Some(value) => flags.insert(flag, value),
317            None => flags.swap_remove(&flag),
318        };
319        files_group_input(db_ref).set_flags(self).to(Some(flags));
320    }
321
322    /// Merges specified [`CfgSet`] into one already stored in this db.
323    fn use_cfg(&mut self, cfg_set: &CfgSet) {
324        let db_ref = self.as_dyn_database();
325        let existing = cfg_set_helper(db_ref);
326        let merged = existing.union(cfg_set);
327        files_group_input(db_ref).set_cfg_set(self).to(Some(merged));
328    }
329
330    /// Returns the cfg set.
331    fn cfg_set(&self) -> &CfgSet {
332        cfg_set_helper(self.as_dyn_database())
333    }
334}
335
336impl<T: Database + ?Sized> FilesGroup for T {}
337
338pub fn init_files_group<'db>(db: &mut (dyn Database + 'db)) {
339    // Initialize inputs.
340    let inp = files_group_input(db);
341    inp.set_file_overrides(db).to(Some(Default::default()));
342    inp.set_crate_configs(db).to(Some(Default::default()));
343    inp.set_flags(db).to(Some(Default::default()));
344    inp.set_cfg_set(db).to(Some(Default::default()));
345}
346
347pub fn set_crate_configs_input(
348    db: &mut dyn Database,
349    crate_configs: Option<OrderedHashMap<CrateInput, CrateConfigurationInput>>,
350) {
351    files_group_input(db).set_crate_configs(db).to(crate_configs);
352}
353
354#[salsa::tracked(returns(ref))]
355pub fn file_overrides<'db>(db: &'db dyn Database) -> OrderedHashMap<FileId<'db>, ArcStr> {
356    let inp = files_group_input(db).file_overrides(db).as_ref().expect("file_overrides is not set");
357    inp.iter()
358        .map(|(file_id, content)| {
359            (file_id.clone().into_file_long_id(db).intern(db), ArcStr::new(content.clone()))
360        })
361        .collect()
362}
363
364#[salsa::tracked(returns(ref))]
365pub fn crate_configs<'db>(
366    db: &'db dyn Database,
367) -> OrderedHashMap<CrateId<'db>, CrateConfiguration<'db>> {
368    let inp = files_group_input(db).crate_configs(db).as_ref().expect("crate_configs is not set");
369    inp.iter()
370        .map(|(crate_input, config)| {
371            (
372                crate_input.clone().into_crate_long_id(db).intern(db),
373                config.clone().into_crate_configuration(db),
374            )
375        })
376        .collect()
377}
378
379#[salsa::tracked(returns(ref))]
380pub fn flags<'db>(db: &'db dyn Database) -> OrderedHashMap<FlagId<'db>, Arc<Flag>> {
381    let inp = files_group_input(db).flags(db).as_ref().expect("flags is not set");
382    inp.iter().map(|(flag_id, flag)| (flag_id.clone().intern(db), flag.clone())).collect()
383}
384
385#[salsa::tracked(returns(ref))]
386fn file_input(db: &dyn Database, file_id: FileId<'_>) -> FileInput {
387    file_id.long(db).into_file_input(db)
388}
389
390#[salsa::tracked(returns(ref))]
391fn crate_input(db: &dyn Database, crt: CrateId<'_>) -> CrateInput {
392    crt.long(db).clone().into_crate_input(db)
393}
394
395#[salsa::tracked(returns(ref))]
396fn crate_configuration_input_helper(
397    db: &dyn Database,
398    _tracked: Tracked,
399    config: CrateConfiguration<'_>,
400) -> CrateConfigurationInput {
401    config.clone().into_crate_configuration_input(db)
402}
403
404fn crate_configuration_input<'db>(
405    db: &'db dyn Database,
406    config: CrateConfiguration<'db>,
407) -> &'db CrateConfigurationInput {
408    crate_configuration_input_helper(db, (), config)
409}
410
411pub fn init_dev_corelib(db: &mut dyn salsa::Database, core_lib_dir: PathBuf) {
412    let core = CrateLongId::core(db).intern(db);
413    let root = CrateConfiguration {
414        root: Directory::Real(core_lib_dir),
415        settings: CrateSettings {
416            name: None,
417            edition: Edition::V2025_12,
418            version: Version::parse(CORELIB_VERSION).ok(),
419            cfg_set: Default::default(),
420            dependencies: Default::default(),
421            experimental_features: ExperimentalFeaturesConfig {
422                negative_impls: true,
423                associated_item_constraints: true,
424                coupons: true,
425                user_defined_inline_macros: true,
426                repr_ptrs: true,
427            },
428        },
429        cache_file: None,
430    };
431    let crate_configs = update_crate_configuration_input_helper(db, core, Some(root));
432    set_crate_configs_input(db, Some(crate_configs));
433}
434
435/// Updates crate configuration input for standalone use.
436pub fn update_crate_configuration_input_helper(
437    db: &dyn Database,
438    crt: CrateId<'_>,
439    root: Option<CrateConfiguration<'_>>,
440) -> OrderedHashMap<CrateInput, CrateConfigurationInput> {
441    let crt = db.crate_input(crt);
442    let db_ref: &dyn Database = db;
443    let mut crate_configs = files_group_input(db_ref).crate_configs(db_ref).clone().unwrap();
444    match root {
445        Some(root) => crate_configs.insert(crt.clone(), db.crate_configuration_input(root).clone()),
446        None => crate_configs.swap_remove(crt),
447    };
448    crate_configs
449}
450
451/// Sets the root directory of the crate. None value removes the crate.
452#[macro_export]
453macro_rules! set_crate_config {
454    ($self:expr, $crt:expr, $root:expr) => {
455        let crate_configs = $crate::db::update_crate_configuration_input_helper($self, $crt, $root);
456        $crate::db::set_crate_configs_input($self, Some(crate_configs));
457    };
458}
459
460/// Updates file overrides input for standalone use.
461pub fn update_file_overrides_input_helper(
462    db: &dyn Database,
463    file: FileInput,
464    content: Option<Arc<str>>,
465) -> OrderedHashMap<FileInput, Arc<str>> {
466    let db_ref: &dyn Database = db;
467    let mut overrides = files_group_input(db_ref).file_overrides(db_ref).clone().unwrap();
468    match content {
469        Some(content) => overrides.insert(file, content),
470        None => overrides.swap_remove(&file),
471    };
472    overrides
473}
474
475/// Overrides file content. None value removes the override.
476#[macro_export]
477macro_rules! override_file_content {
478    ($self:expr, $file:expr, $content:expr) => {
479        let file = $self.file_input($file).clone();
480        let overrides = $crate::db::update_file_overrides_input_helper($self, file, $content);
481        salsa::Setter::to(
482            $crate::db::files_group_input($self).set_file_overrides($self),
483            Some(overrides),
484        );
485    };
486}
487
488fn cfg_set_helper(db: &dyn Database) -> &CfgSet {
489    files_group_input(db).cfg_set(db).as_ref().expect("cfg_set is not set")
490}
491
492#[salsa::tracked(returns(ref))]
493fn crates<'db>(db: &'db dyn Database) -> Vec<CrateId<'db>> {
494    // TODO(spapini): Sort for stability.
495    db.crate_configs().keys().copied().collect()
496}
497
498/// Tracked function to return the configuration of a crate.
499#[salsa::tracked(returns(ref))]
500fn crate_config_helper<'db>(
501    db: &'db dyn Database,
502    crt: CrateId<'db>,
503) -> Option<CrateConfiguration<'db>> {
504    match crt.long(db) {
505        CrateLongId::Real { .. } => db.crate_configs().get(&crt).cloned(),
506        CrateLongId::Virtual { name: _, file_id, settings, cache_file } => {
507            Some(CrateConfiguration {
508                root: Directory::Virtual {
509                    files: BTreeMap::from([("lib.cairo".to_string(), *file_id)]),
510                    dirs: Default::default(),
511                },
512                settings: toml::from_str(settings)
513                    .expect("Failed to parse virtual crate settings."),
514                cache_file: *cache_file,
515            })
516        }
517    }
518}
519
520/// Returns a reference to the configuration of a crate.
521/// This is a wrapper around the tracked function `crate_config_helper` to return a
522/// reference to a type unsupported by salsa tracked functions.
523fn crate_config<'db>(
524    db: &'db dyn Database,
525    crt: CrateId<'db>,
526) -> Option<&'db CrateConfiguration<'db>> {
527    crate_config_helper(db, crt).as_ref()
528}
529
530#[salsa::tracked]
531fn priv_raw_file_content<'db>(db: &'db dyn Database, file: FileId<'db>) -> Option<SmolStrId<'db>> {
532    match file.long(db) {
533        FileLongId::OnDisk(path) => {
534            // This does not result in performance cost due to OS caching and the fact that salsa
535            // will re-execute only this single query if the file content did not change.
536            db.report_untracked_read();
537
538            match fs::read_to_string(path) {
539                Ok(content) => Some(SmolStrId::new(db, SmolStr::new(content))),
540                Err(_) => None,
541            }
542        }
543        FileLongId::Virtual(virt) => Some(virt.content),
544        FileLongId::External(external_id) => Some(ext_as_virtual(db, *external_id).content),
545    }
546}
547
548/// Tracked function to return the content of a file as a string.
549#[salsa::tracked(returns(ref))]
550fn file_summary_helper<'db>(db: &'db dyn Database, file: FileId<'db>) -> Option<FileSummary> {
551    let content = db.file_content(file)?;
552    let mut line_offsets = vec![TextOffset::START];
553    let mut offset = TextOffset::START;
554    for ch in content.chars() {
555        offset = offset.add_width(TextWidth::from_char(ch));
556        if ch == '\n' {
557            line_offsets.push(offset);
558        }
559    }
560    Some(FileSummary { line_offsets, last_offset: offset })
561}
562
563/// Query implementation of [FilesGroup::file_content].
564#[salsa::tracked(returns(ref))]
565fn file_content<'db>(db: &'db dyn Database, file_id: FileId<'db>) -> Option<Arc<str>> {
566    let overrides = db.file_overrides();
567    overrides.get(&file_id).map(|content| (**content).clone()).or_else(|| {
568        priv_raw_file_content(db, file_id).map(|content| content.long(db).clone().into())
569    })
570}
571
572/// Returns a reference to the content of a file as a string.
573/// This is a wrapper around the tracked function `file_summary_helper` to return a
574/// reference to a type unsupported by salsa tracked functions.
575fn file_summary<'db>(db: &'db dyn Database, file: FileId<'db>) -> Option<&'db FileSummary> {
576    file_summary_helper(db, file).as_ref()
577}
578
579/// Returns a reference to the flag value.
580#[salsa::tracked(returns(ref))]
581fn get_flag_helper<'db>(db: &'db dyn Database, id: FlagId<'db>) -> Option<Arc<Flag>> {
582    db.flags().get(&id).cloned()
583}
584
585/// Returns a reference to the flag value.
586// TODO(eytan-starkware): Remove helper function and use flags here.
587fn get_flag<'db>(db: &'db dyn Database, id: FlagId<'db>) -> Option<&'db Flag> {
588    db.flags().get(&id).map(|flag| flag.as_ref())
589}
590
591/// Tracked function to return the blob's content.
592#[salsa::tracked(returns(ref))]
593fn blob_content_helper<'db>(db: &'db dyn Database, blob: BlobId<'db>) -> Option<Vec<u8>> {
594    blob.long(db).content()
595}
596
597/// Wrapper around the tracked function `blob_content_helper` to return a
598/// reference to a type unsupported by salsa tracked functions.
599fn blob_content<'db>(db: &'db dyn Database, blob: BlobId<'db>) -> Option<&'db [u8]> {
600    blob_content_helper(db, blob).as_ref().map(|content| content.as_slice())
601}
602
603/// Returns the location of the originating user code.
604pub fn get_originating_location<'db>(
605    db: &'db dyn Database,
606    mut location: SpanInFile<'db>,
607    mut parent_files: Option<&mut Vec<FileId<'db>>>,
608) -> SpanInFile<'db> {
609    if let Some(ref mut parent_files) = parent_files {
610        parent_files.push(location.file_id);
611    }
612    while let Some((parent, code_mappings)) = get_parent_and_mapping(db, location.file_id) {
613        location.file_id = parent.file_id;
614        if let Some(ref mut parent_files) = parent_files {
615            parent_files.push(location.file_id);
616        }
617        location.span = translate_location(code_mappings, location.span).unwrap_or(parent.span);
618    }
619    location
620}
621
622/// This function finds a span in original code that corresponds to the provided span in the
623/// generated code, using the provided code mappings.
624///
625/// Code mappings describe a mapping between the original code and the generated one.
626/// Each mapping has a resulting span in a generated file and an origin in the original file.
627///
628/// If any of the provided mappings fully contains the span, origin span of the mapping will be
629/// returned. Otherwise, the function will try to find a span that is a result of a concatenation of
630/// multiple consecutive mappings.
631pub fn translate_location(code_mapping: &[CodeMapping], span: TextSpan) -> Option<TextSpan> {
632    // If any of the mappings fully contains the span, return the origin span of the mapping.
633    if let Some(containing) = code_mapping.iter().find(|mapping| {
634        mapping.span.contains(span) && !matches!(mapping.origin, CodeOrigin::CallSite(_))
635    }) {
636        // Found a span that fully contains the current one - translates it.
637        return containing.translate(span);
638    }
639
640    // Find all mappings that have non-empty intersection with the provided span.
641    let intersecting_mappings = || {
642        code_mapping.iter().filter(|mapping| {
643            // Omit mappings to the left or to the right of current span.
644            mapping.span.end > span.start && mapping.span.start < span.end
645        })
646    };
647
648    // Call site can be treated as default origin.
649    let call_site = intersecting_mappings()
650        .find(|mapping| {
651            mapping.span.contains(span) && matches!(mapping.origin, CodeOrigin::CallSite(_))
652        })
653        .and_then(|containing| containing.translate(span));
654
655    let mut matched = intersecting_mappings()
656        .filter(|mapping| matches!(mapping.origin, CodeOrigin::Span(_)))
657        .collect::<Vec<_>>();
658
659    // If no mappings intersect with the span, translation is impossible.
660    if matched.is_empty() {
661        return call_site;
662    }
663
664    // Take the first mapping to the left.
665    matched.sort_by_key(|mapping| mapping.span);
666    let (first, matched) = matched.split_first().expect("non-empty vec always has first element");
667
668    // Find the last mapping which consecutively follows the first one.
669    // Note that all spans here intersect with the given one.
670    let mut last = first;
671    for mapping in matched {
672        if mapping.span.start > last.span.end {
673            break;
674        }
675
676        let mapping_origin =
677            mapping.origin.as_span().expect("mappings with start origin should be filtered out");
678        let last_origin =
679            last.origin.as_span().expect("mappings with start origin should be filtered out");
680        // Make sure, the origins are consecutive.
681        if mapping_origin.start > last_origin.end {
682            break;
683        }
684
685        last = mapping;
686    }
687
688    // We construct new span from the first and last mappings.
689    // If the new span does not contain the original span, there is no translation.
690    let constructed_span = TextSpan::new(first.span.start, last.span.end);
691    if !constructed_span.contains(span) {
692        return call_site;
693    }
694
695    // We use the boundaries of the first and last mappings to calculate new span origin.
696    let start = match first.origin {
697        CodeOrigin::Start(origin_start) => origin_start.add_width(span.start - first.span.start),
698        CodeOrigin::Span(span) => span.start,
699        CodeOrigin::CallSite(span) => span.start,
700    };
701
702    let end = match last.origin {
703        CodeOrigin::Start(_) => start.add_width(span.width()),
704        CodeOrigin::Span(span) => span.end,
705        CodeOrigin::CallSite(span) => span.start,
706    };
707
708    Some(TextSpan::new(start, end))
709}
710
711/// Returns the parent file and the code mappings of the file.
712pub fn get_parent_and_mapping<'db>(
713    db: &'db dyn Database,
714    file_id: FileId<'db>,
715) -> Option<(SpanInFile<'db>, &'db [CodeMapping])> {
716    let vf = match file_id.long(db) {
717        FileLongId::OnDisk(_) => return None,
718        FileLongId::Virtual(vf) => vf,
719        FileLongId::External(id) => ext_as_virtual(db, *id),
720    };
721    Some((vf.parent?, &vf.code_mappings))
722}
723
724/// Returns the virtual file matching the external id. Panics if the id is not found.
725pub fn ext_as_virtual<'db>(db: &'db dyn Database, id: salsa::Id) -> &'db VirtualFile<'db> {
726    files_group_input(db)
727        .ext_as_virtual_obj(db)
728        .as_ref()
729        .expect("`ext_as_virtual` was not set as input.")(db, id)
730}
731
732/// Non-pub queries over the files group.
733trait PrivFilesGroup: Database {
734    /// Create an input crate configuration from a [`CrateConfiguration`].
735    fn crate_configuration_input<'db>(
736        &'db self,
737        config: CrateConfiguration<'db>,
738    ) -> &'db CrateConfigurationInput {
739        crate_configuration_input(self.as_dyn_database(), config)
740    }
741}
742
743impl<T: Database + ?Sized> PrivFilesGroup for T {}