Skip to main content

runmat_package/source/
symbols.rs

1use super::{discover_frozen_project_from, discover_frozen_project_from_async, FrozenProject};
2use crate::{ContentDigest, FrozenProjectError, StableSourceId};
3use runmat_config::project::{
4    build_loose_source_index, build_loose_source_index_async, ProjectSourceFile,
5    ProjectSourceIndex, ProjectSourceIndexError,
6};
7use serde::{Deserialize, Serialize};
8use std::collections::{BTreeMap, BTreeSet, HashSet};
9use std::path::{Path, PathBuf};
10use thiserror::Error;
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13pub struct ProjectSymbolDefinition {
14    pub name: String,
15    pub qualified_name: String,
16    pub source_path: PathBuf,
17    pub package_name: String,
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub dependency_alias: Option<String>,
20    #[serde(skip_serializing_if = "Option::is_none")]
21    pub package_instance: Option<ContentDigest>,
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub source_id: Option<StableSourceId>,
24    pub is_private: bool,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct DiscoveredSourceSymbols {
29    pub manifest_path: Option<PathBuf>,
30    pub project_root: PathBuf,
31    pub graph_digest: Option<ContentDigest>,
32    pub source_revision: Option<ContentDigest>,
33    pub symbols: HashSet<String>,
34    pub definitions: Vec<ProjectSymbolDefinition>,
35}
36
37impl DiscoveredSourceSymbols {
38    pub fn project_revision(&self) -> Option<crate::ProjectRevision> {
39        Some(crate::ProjectRevision {
40            graph_digest: self.graph_digest.clone()?,
41            source_revision: self.source_revision.clone()?,
42        })
43    }
44}
45
46#[derive(Debug, Error)]
47pub enum DiscoverSourceSymbolsError {
48    #[error(transparent)]
49    Frozen(#[from] FrozenProjectError),
50    #[error("failed to index loose MATLAB sources under {root}: {source}")]
51    LooseSourceIndex {
52        root: PathBuf,
53        #[source]
54        source: ProjectSourceIndexError,
55    },
56}
57
58pub fn discover_source_symbols_from_source_name(
59    source_name: &str,
60    cwd: &Path,
61) -> Result<Option<DiscoveredSourceSymbols>, DiscoverSourceSymbolsError> {
62    let Some((source_path, root)) = local_source_and_parent(source_name, cwd) else {
63        return Ok(None);
64    };
65    if let Some(frozen) = discover_frozen_project_from(&source_path, BTreeSet::new())? {
66        return Ok(Some(source_symbols_from_frozen(&frozen, &source_path)));
67    }
68    let index = build_loose_source_index(&root).map_err(|source| {
69        DiscoverSourceSymbolsError::LooseSourceIndex {
70            root: root.clone(),
71            source,
72        }
73    })?;
74    Ok(Some(source_symbols_from_index(
75        &index,
76        &root,
77        &source_path,
78        None,
79    )))
80}
81
82pub async fn discover_source_symbols_from_source_name_async(
83    source_name: &str,
84    cwd: &Path,
85) -> Result<Option<DiscoveredSourceSymbols>, DiscoverSourceSymbolsError> {
86    let Some((source_path, root)) = local_source_and_parent_async(source_name, cwd).await else {
87        return Ok(None);
88    };
89    if let Some(frozen) = discover_frozen_project_from_async(&source_path, BTreeSet::new()).await? {
90        return Ok(Some(source_symbols_from_frozen(&frozen, &source_path)));
91    }
92    let index = build_loose_source_index_async(&root)
93        .await
94        .map_err(|source| DiscoverSourceSymbolsError::LooseSourceIndex {
95            root: root.clone(),
96            source,
97        })?;
98    Ok(Some(source_symbols_from_index(
99        &index,
100        &root,
101        &source_path,
102        None,
103    )))
104}
105
106pub fn source_symbols_from_index(
107    index: &ProjectSourceIndex,
108    root: &Path,
109    primary_source: &Path,
110    manifest_path: Option<PathBuf>,
111) -> DiscoveredSourceSymbols {
112    let mut symbols = HashSet::new();
113    let mut definitions = Vec::new();
114    for source in &index.files {
115        extend_loose_source(&mut symbols, &mut definitions, source, root);
116    }
117    add_visible_private_symbols(&mut symbols, &definitions, primary_source);
118    DiscoveredSourceSymbols {
119        manifest_path,
120        project_root: root.to_path_buf(),
121        graph_digest: None,
122        source_revision: None,
123        symbols,
124        definitions,
125    }
126}
127
128pub fn discover_known_project_symbols_from_source_name(
129    source_name: Option<&str>,
130    cwd: &Path,
131) -> HashSet<String> {
132    source_name
133        .and_then(|source_name| {
134            discover_source_symbols_from_source_name(source_name, cwd)
135                .ok()
136                .flatten()
137        })
138        .map(|discovered| discovered.symbols)
139        .unwrap_or_default()
140}
141
142pub async fn discover_known_project_symbols_from_source_name_async(
143    source_name: Option<&str>,
144    cwd: &Path,
145) -> HashSet<String> {
146    let Some(source_name) = source_name else {
147        return HashSet::new();
148    };
149    discover_source_symbols_from_source_name_async(source_name, cwd)
150        .await
151        .ok()
152        .flatten()
153        .map(|discovered| discovered.symbols)
154        .unwrap_or_default()
155}
156
157pub fn source_symbols_from_frozen(
158    frozen: &FrozenProject,
159    primary_source: &Path,
160) -> DiscoveredSourceSymbols {
161    let mut symbols = HashSet::new();
162    let mut definitions = Vec::new();
163    let mut unqualified_candidates =
164        BTreeMap::<String, BTreeMap<ContentDigest, ProjectSymbolDefinition>>::new();
165    for visible in frozen.visible_sources(primary_source) {
166        if !visible.directly_visible {
167            continue;
168        }
169        let alias = visible.dependency_alias.map(ToString::to_string);
170        for name in source_names(
171            &visible.source.qualified_name,
172            visible.source.class_definition_qualified_name(),
173        ) {
174            if let Some(alias) = &alias {
175                let exposed = format!("{alias}.{name}");
176                push_definition(
177                    &mut definitions,
178                    exposed.clone(),
179                    visible.source,
180                    visible.access_path,
181                    &visible.package.local_name,
182                    Some(alias),
183                );
184                symbols.insert(exposed);
185                unqualified_candidates.entry(name).or_default().insert(
186                    visible.source.id.package_instance.clone(),
187                    project_definition(
188                        visible.source.qualified_name.clone(),
189                        visible.source,
190                        visible.access_path,
191                        &visible.package.local_name,
192                        Some(alias),
193                    ),
194                );
195            } else {
196                push_definition(
197                    &mut definitions,
198                    name.clone(),
199                    visible.source,
200                    visible.access_path,
201                    &visible.package.local_name,
202                    None,
203                );
204                symbols.insert(name);
205            }
206        }
207    }
208    for (name, candidates) in unqualified_candidates {
209        if candidates.len() == 1 && !symbols.contains(&name) {
210            let mut definition = candidates
211                .into_values()
212                .next()
213                .expect("one unqualified candidate");
214            definition.name = name.clone();
215            definitions.push(definition);
216            symbols.insert(name);
217        }
218    }
219    DiscoveredSourceSymbols {
220        manifest_path: Some(frozen.manifest_path.clone()),
221        project_root: frozen.workspace_root.clone(),
222        graph_digest: Some(frozen.graph.graph_digest.clone()),
223        source_revision: Some(frozen.sources.revision.clone()),
224        symbols,
225        definitions,
226    }
227}
228
229fn push_definition(
230    definitions: &mut Vec<ProjectSymbolDefinition>,
231    name: String,
232    source: &crate::FrozenSourceDescriptor,
233    source_path: &Path,
234    package_name: &str,
235    dependency_alias: Option<&str>,
236) {
237    if definitions
238        .iter()
239        .any(|definition| definition.name == name && definition.source_path == source_path)
240    {
241        return;
242    }
243    definitions.push(project_definition(
244        name,
245        source,
246        source_path,
247        package_name,
248        dependency_alias,
249    ));
250}
251
252fn project_definition(
253    name: String,
254    source: &crate::FrozenSourceDescriptor,
255    source_path: &Path,
256    package_name: &str,
257    dependency_alias: Option<&str>,
258) -> ProjectSymbolDefinition {
259    ProjectSymbolDefinition {
260        name,
261        qualified_name: source.qualified_name.clone(),
262        source_path: source_path.to_path_buf(),
263        package_name: package_name.to_string(),
264        dependency_alias: dependency_alias.map(ToOwned::to_owned),
265        package_instance: Some(source.id.package_instance.clone()),
266        source_id: Some(source.id.clone()),
267        is_private: source.is_private,
268    }
269}
270
271fn extend_loose_source(
272    symbols: &mut HashSet<String>,
273    definitions: &mut Vec<ProjectSymbolDefinition>,
274    source: &ProjectSourceFile,
275    root: &Path,
276) {
277    let source_path = root.join(&source.source_root).join(&source.relative_path);
278    for name in source_names(
279        &source.qualified_name,
280        source.class_definition_qualified_name(),
281    ) {
282        definitions.push(ProjectSymbolDefinition {
283            name: name.clone(),
284            qualified_name: source.qualified_name.clone(),
285            source_path: source_path.clone(),
286            package_name: String::new(),
287            dependency_alias: None,
288            package_instance: None,
289            source_id: None,
290            is_private: source.is_private,
291        });
292        if !source.is_private {
293            symbols.insert(name);
294        }
295    }
296}
297
298fn source_names(qualified_name: &str, class_name: Option<&str>) -> Vec<String> {
299    let mut names = vec![qualified_name.to_string()];
300    if let Some(class_name) = class_name {
301        if class_name != qualified_name {
302            names.push(class_name.to_string());
303        }
304    }
305    names
306}
307
308fn add_visible_private_symbols(
309    symbols: &mut HashSet<String>,
310    definitions: &[ProjectSymbolDefinition],
311    primary_source: &Path,
312) {
313    let primary_parent = primary_source.parent();
314    for definition in definitions
315        .iter()
316        .filter(|definition| definition.is_private)
317    {
318        let private_owner = definition.source_path.parent().and_then(Path::parent);
319        if private_owner.is_some() && private_owner == primary_parent {
320            symbols.insert(definition.name.clone());
321        }
322    }
323}
324
325fn local_source_and_parent(source_name: &str, cwd: &Path) -> Option<(PathBuf, PathBuf)> {
326    let source_path = PathBuf::from(source_name);
327    let local = absolute_or_join(cwd, &source_path);
328    if (source_name.contains(':') && !local.exists()) || !local.is_file() {
329        return None;
330    }
331    Some((local.clone(), local.parent()?.to_path_buf()))
332}
333
334async fn local_source_and_parent_async(
335    source_name: &str,
336    cwd: &Path,
337) -> Option<(PathBuf, PathBuf)> {
338    let source_path = PathBuf::from(source_name);
339    let local = absolute_or_join(cwd, &source_path);
340    if (source_name.contains(':') && runmat_filesystem::metadata_async(&local).await.is_err())
341        || !runmat_filesystem::metadata_async(&local)
342            .await
343            .is_ok_and(|metadata| metadata.is_file())
344    {
345        return None;
346    }
347    Some((local.clone(), local.parent()?.to_path_buf()))
348}
349
350fn absolute_or_join(cwd: &Path, path: &Path) -> PathBuf {
351    if path.is_absolute() {
352        path.to_path_buf()
353    } else {
354        cwd.join(path)
355    }
356}