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}