1use dashmap::DashMap;
7use shape_ast::ast::{Program, Span};
8#[cfg(test)]
9use shape_ast::parser::parse_program;
10use std::collections::HashMap;
11use std::path::{Path, PathBuf};
12use std::sync::Arc;
13
14fn module_path_segments(path: &str) -> Vec<&str> {
15 if path.contains("::") {
16 path.split("::")
17 .filter(|segment| !segment.is_empty())
18 .collect()
19 } else {
20 path.split('.')
21 .filter(|segment| !segment.is_empty())
22 .collect()
23 }
24}
25
26fn is_std_module_path(path: &str) -> bool {
27 module_path_segments(path)
28 .first()
29 .is_some_and(|segment| *segment == "std")
30}
31
32#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum SymbolKind {
35 Function,
36 Pattern,
37 Variable,
38 TypeAlias,
39 Interface,
40 Enum,
41 Annotation,
42}
43
44#[derive(Debug, Clone)]
46pub struct ExportedSymbol {
47 pub name: String,
49 pub alias: Option<String>,
51 pub kind: SymbolKind,
53 pub span: Span,
55}
56
57impl ExportedSymbol {
58 pub fn exported_name(&self) -> &str {
60 self.alias.as_ref().unwrap_or(&self.name)
61 }
62}
63
64#[derive(Debug, Clone)]
66pub struct ModuleInfo {
67 pub path: PathBuf,
69 pub program: Arc<Program>,
71 pub exports: Vec<ExportedSymbol>,
73}
74
75#[derive(Debug, Default)]
77pub struct ModuleCache {
78 modules: DashMap<PathBuf, ModuleInfo>,
80}
81
82impl ModuleCache {
83 pub fn new() -> Self {
85 Self {
86 modules: DashMap::new(),
87 }
88 }
89
90 fn loader_for_context(
91 current_file: &Path,
92 workspace_root: Option<&Path>,
93 current_source: Option<&str>,
94 ) -> shape_runtime::module_loader::ModuleLoader {
95 let mut loader = shape_runtime::module_loader::ModuleLoader::new();
96 loader.configure_for_context_with_source(current_file, workspace_root, current_source);
97 loader
98 }
99
100 pub fn resolve_import(
102 &self,
103 import_path: &str,
104 current_file: &Path,
105 workspace_root: Option<&Path>,
106 ) -> Option<PathBuf> {
107 let loader = Self::loader_for_context(current_file, workspace_root, None);
108
109 let context_dir = current_file.parent().map(Path::to_path_buf);
110 let resolved = loader.resolve_module_path_with_context(import_path, context_dir.as_ref());
111 if let Ok(path) = resolved {
112 return Some(path);
113 }
114
115 if import_path.contains("::")
117 || import_path.starts_with("./")
118 || import_path.starts_with("../")
119 || import_path.starts_with('/')
120 {
121 return None;
122 }
123
124 let canonical = import_path.replace('.', "::");
125 loader
126 .resolve_module_path_with_context(&canonical, context_dir.as_ref())
127 .ok()
128 }
129
130 pub fn load_module(&self, path: &Path) -> Option<ModuleInfo> {
135 self.load_module_with_context(path, path, None)
136 }
137
138 pub fn load_module_with_context(
140 &self,
141 path: &Path,
142 current_file: &Path,
143 workspace_root: Option<&Path>,
144 ) -> Option<ModuleInfo> {
145 if let Some(cached) = self.modules.get(path) {
147 return Some(cached.clone());
148 }
149
150 let mut loader = Self::loader_for_context(current_file, workspace_root, None);
152 let module = loader.load_module_from_file(path).ok()?;
153 let program = Arc::new(module.ast.clone());
154
155 let exports = extract_exports(&program);
157
158 let module_info = ModuleInfo {
159 path: path.to_path_buf(),
160 program: program.clone(),
161 exports,
162 };
163
164 self.modules.insert(path.to_path_buf(), module_info.clone());
166
167 Some(module_info)
168 }
169
170 pub fn load_module_by_import_with_context_and_source(
174 &self,
175 import_path: &str,
176 current_file: &Path,
177 workspace_root: Option<&Path>,
178 current_source: Option<&str>,
179 ) -> Option<ModuleInfo> {
180 let mut loader = Self::loader_for_context(current_file, workspace_root, current_source);
181 let context_dir = current_file.parent().map(Path::to_path_buf);
182 let module = loader
183 .load_module_with_context(import_path, context_dir.as_ref())
184 .ok()?;
185
186 let cache_path = PathBuf::from(format!(
187 "__shape_lsp_virtual__/{}.shape",
188 import_path.replace("::", "/").replace('.', "/")
189 ));
190 let program = Arc::new(module.ast.clone());
191 let exports = extract_exports(&program);
192 let module_info = ModuleInfo {
193 path: cache_path.clone(),
194 program: program.clone(),
195 exports,
196 };
197 self.modules.insert(cache_path, module_info.clone());
198 Some(module_info)
199 }
200
201 pub fn get_module(&self, path: &Path) -> Option<ModuleInfo> {
203 self.modules.get(path).map(|entry| entry.clone())
204 }
205
206 pub fn invalidate(&self, path: &Path) {
208 self.modules.remove(path);
209 }
210
211 pub fn clear(&self) {
213 self.modules.clear();
214 }
215
216 pub fn list_importable_modules_with_context(
223 &self,
224 current_file: &Path,
225 workspace_root: Option<&Path>,
226 ) -> Vec<String> {
227 self.list_importable_modules_with_context_and_source(current_file, workspace_root, None)
228 }
229
230 pub fn list_importable_modules_with_context_and_source(
232 &self,
233 current_file: &Path,
234 workspace_root: Option<&Path>,
235 current_source: Option<&str>,
236 ) -> Vec<String> {
237 let mut loader = shape_runtime::module_loader::ModuleLoader::new();
238 loader.configure_for_context_with_source(current_file, workspace_root, current_source);
239 loader.list_importable_modules_with_context(current_file, workspace_root)
240 }
241
242 pub fn list_importable_modules(&self) -> Vec<String> {
244 let current_file = std::env::current_dir()
245 .unwrap_or_else(|_| PathBuf::from("."))
246 .join("__shape_lsp__.shape");
247 self.list_importable_modules_with_context(¤t_file, None)
248 }
249
250 pub fn list_stdlib_modules(&self) -> Vec<String> {
252 self.list_importable_modules()
253 .into_iter()
254 .filter(|module_path| is_std_module_path(module_path))
255 .collect()
256 }
257
258 pub fn list_stdlib_children(&self, prefix: &str) -> Vec<ModuleChild> {
264 let effective_prefix = if prefix.is_empty() { "std" } else { prefix };
265 if !is_std_module_path(effective_prefix) {
266 return Vec::new();
267 }
268
269 self.list_module_children(effective_prefix)
270 }
271
272 pub fn list_module_children_with_context(
274 &self,
275 prefix: &str,
276 current_file: &Path,
277 workspace_root: Option<&Path>,
278 ) -> Vec<ModuleChild> {
279 let base = if prefix.is_empty() {
280 "std".to_string()
281 } else {
282 prefix.to_string()
283 };
284
285 let mut children: HashMap<String, ModuleChild> = HashMap::new();
286 let base_segments = module_path_segments(&base);
287 let base_len = base_segments.len();
288 for module_path in self.list_importable_modules_with_context(current_file, workspace_root) {
289 let module_segments = module_path_segments(&module_path);
290 if module_segments.len() <= base_len {
291 continue;
292 }
293 if module_segments[..base_len] != base_segments[..] {
294 continue;
295 }
296
297 let child = module_segments[base_len];
298 let has_children = module_segments.len() > base_len + 1;
299
300 let entry = children.entry(child.to_string()).or_insert(ModuleChild {
301 name: child.to_string(),
302 has_leaf_module: false,
303 has_children: false,
304 });
305 if has_children {
306 entry.has_children = true;
307 } else {
308 entry.has_leaf_module = true;
309 }
310 }
311
312 let mut out: Vec<ModuleChild> = children.into_values().collect();
313 out.sort_by(|a, b| a.name.cmp(&b.name));
314 out
315 }
316
317 pub fn list_module_children(&self, prefix: &str) -> Vec<ModuleChild> {
319 let current_file = std::env::current_dir()
320 .unwrap_or_else(|_| PathBuf::from("."))
321 .join("__shape_lsp__.shape");
322 self.list_module_children_with_context(prefix, ¤t_file, None)
323 }
324
325 pub fn find_exported_symbol_with_context(
328 &self,
329 name: &str,
330 current_file: &Path,
331 workspace_root: Option<&Path>,
332 ) -> Vec<(String, ExportedSymbol)> {
333 let mut results = Vec::new();
334
335 for import_path in self.list_importable_modules_with_context(current_file, workspace_root) {
336 let Some(resolved) = self.resolve_import(&import_path, current_file, workspace_root)
337 else {
338 continue;
339 };
340 let Some(module_info) =
341 self.load_module_with_context(&resolved, current_file, workspace_root)
342 else {
343 continue;
344 };
345
346 for export in &module_info.exports {
347 if export.exported_name() == name {
348 results.push((import_path.clone(), export.clone()));
349 }
350 }
351 }
352
353 results
354 }
355
356 pub fn find_exported_symbol(&self, name: &str) -> Vec<(String, ExportedSymbol)> {
358 let current_file = std::env::current_dir()
359 .unwrap_or_else(|_| PathBuf::from("."))
360 .join("__shape_lsp__.shape");
361 self.find_exported_symbol_with_context(name, ¤t_file, None)
362 }
363}
364
365#[derive(Debug, Clone)]
367pub struct ModuleChild {
368 pub name: String,
369 pub has_leaf_module: bool,
370 pub has_children: bool,
371}
372
373fn map_module_export_kind(kind: shape_runtime::module_loader::ModuleExportKind) -> SymbolKind {
374 use shape_runtime::module_loader::ModuleExportKind as RuntimeKind;
375 match kind {
376 RuntimeKind::Function => SymbolKind::Function,
377 RuntimeKind::TypeAlias => SymbolKind::TypeAlias,
378 RuntimeKind::Interface => SymbolKind::Interface,
379 RuntimeKind::Enum => SymbolKind::Enum,
380 RuntimeKind::Value => SymbolKind::Variable,
381 }
382}
383
384fn extract_exports(program: &Program) -> Vec<ExportedSymbol> {
386 shape_runtime::module_loader::collect_exported_symbols(program)
387 .unwrap_or_default()
388 .into_iter()
389 .map(|sym| ExportedSymbol {
390 name: sym.name,
391 alias: sym.alias,
392 kind: map_module_export_kind(sym.kind),
393 span: sym.span,
394 })
395 .collect()
396}
397
398#[cfg(test)]
399mod tests {
400 use super::*;
401
402 #[test]
403 fn test_resolve_stdlib_import() {
404 let cache = ModuleCache::new();
405 let current_file =
406 PathBuf::from("/home/dev/dev/finance/analysis-suite/shape/examples/test.shape");
407
408 let resolved = cache.resolve_import("std::core::math", ¤t_file, None);
409
410 assert!(resolved.is_some());
411 let path = resolved.unwrap();
412 let path_str = path.to_string_lossy();
413 assert!(path_str.contains("stdlib/core/math.shape"));
414 }
415
416 #[test]
417 fn test_relative_import_is_supported() {
418 let tmp = tempfile::tempdir().unwrap();
419 let current_file = tmp.path().join("main.shape");
420 let util = tmp.path().join("utils.shape");
421 std::fs::write(¤t_file, "from ./utils use { helper }").unwrap();
422 std::fs::write(&util, "pub fn helper() { 1 }").unwrap();
423
424 let cache = ModuleCache::new();
425
426 let resolved = cache.resolve_import("./utils", ¤t_file, None);
427 assert_eq!(resolved.as_deref(), Some(util.as_path()));
428 }
429
430 #[test]
431 fn test_non_std_import_returns_none() {
432 let cache = ModuleCache::new();
433 let current_file = PathBuf::from("/home/user/project/src/main.shape");
434
435 let resolved = cache.resolve_import("finance::indicators", ¤t_file, None);
437 assert!(resolved.is_none());
438 }
439
440 #[test]
441 fn test_exported_symbol_name() {
442 let symbol = ExportedSymbol {
443 name: "originalName".to_string(),
444 alias: Some("aliasName".to_string()),
445 kind: SymbolKind::Function,
446 span: Span::default(),
447 };
448
449 assert_eq!(symbol.exported_name(), "aliasName");
450
451 let symbol_no_alias = ExportedSymbol {
452 name: "originalName".to_string(),
453 alias: None,
454 kind: SymbolKind::Function,
455 span: Span::default(),
456 };
457
458 assert_eq!(symbol_no_alias.exported_name(), "originalName");
459 }
460
461 #[test]
462 fn test_extract_exports() {
463 let source = r#"
464pub fn myFunc(x) {
465 return x + 1;
466}
467
468fn localFunc() {
469 return 42;
470}
471"#;
472
473 let program = parse_program(source).unwrap();
474 let exports = extract_exports(&program);
475
476 assert_eq!(exports.len(), 1);
477 assert_eq!(exports[0].name, "myFunc");
478 assert_eq!(exports[0].kind, SymbolKind::Function);
479 }
480
481 #[test]
482 fn test_list_stdlib_modules_not_empty() {
483 let cache = ModuleCache::new();
484 let modules = cache.list_stdlib_modules();
485 assert!(
486 !modules.is_empty(),
487 "expected stdlib module list to be non-empty"
488 );
489 assert!(
490 modules.iter().all(|m| m.starts_with("std::")),
491 "all stdlib modules should be std::-prefixed: {:?}",
492 modules
493 );
494 }
495
496 #[test]
497 fn test_list_stdlib_children_for_std_prefix() {
498 let cache = ModuleCache::new();
499 let children = cache.list_stdlib_children("std");
500 assert!(
501 !children.is_empty(),
502 "expected stdlib root to have child modules"
503 );
504 assert!(
505 children.iter().any(|c| c.name == "core"),
506 "expected std.core child in stdlib tree"
507 );
508 }
509
510 #[test]
511 fn test_list_importable_modules_with_project_modules_and_deps() {
512 let tmp = tempfile::tempdir().unwrap();
513 let root = tmp.path();
514 std::fs::write(
515 root.join("shape.toml"),
516 r#"
517[modules]
518paths = ["lib"]
519
520[dependencies]
521mydep = { path = "deps/mydep" }
522"#,
523 )
524 .unwrap();
525
526 std::fs::create_dir_all(root.join("src")).unwrap();
527 std::fs::create_dir_all(root.join("lib")).unwrap();
528 std::fs::create_dir_all(root.join("deps/mydep")).unwrap();
529
530 std::fs::write(root.join("src/main.shape"), "let x = 1").unwrap();
531 std::fs::write(root.join("lib/tools.shape"), "pub fn tool() { 1 }").unwrap();
532 std::fs::write(root.join("deps/mydep/index.shape"), "pub fn root() { 1 }").unwrap();
533 std::fs::write(root.join("deps/mydep/util.shape"), "pub fn util() { 1 }").unwrap();
534
535 let cache = ModuleCache::new();
536 let modules =
537 cache.list_importable_modules_with_context(&root.join("src/main.shape"), None);
538
539 assert!(
540 modules.iter().any(|m| m == "tools"),
541 "expected module path from [modules].paths, got: {:?}",
542 modules
543 );
544 assert!(
545 modules.iter().any(|m| m == "mydep"),
546 "expected dependency index module path, got: {:?}",
547 modules
548 );
549 assert!(
550 modules.iter().any(|m| m == "mydep::util"),
551 "expected dependency submodule path, got: {:?}",
552 modules
553 );
554 }
555
556 #[test]
557 fn test_module_cache_invalidation() {
558 let cache = ModuleCache::new();
559 let path = PathBuf::from("/test/module.shape");
560
561 let program = Arc::new(Program {
563 items: vec![],
564 docs: shape_ast::ast::ProgramDocs::default(),
565 });
566 let module_info = ModuleInfo {
567 path: path.clone(),
568 program,
569 exports: vec![],
570 };
571
572 cache.modules.insert(path.clone(), module_info.clone());
574 assert!(cache.get_module(&path).is_some());
575
576 cache.invalidate(&path);
578 assert!(cache.get_module(&path).is_none());
579 }
580}