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::BuiltinFunction => SymbolKind::Function,
378 RuntimeKind::TypeAlias => SymbolKind::TypeAlias,
379 RuntimeKind::BuiltinType => SymbolKind::TypeAlias,
380 RuntimeKind::Interface => SymbolKind::Interface,
381 RuntimeKind::Enum => SymbolKind::Enum,
382 RuntimeKind::Annotation => SymbolKind::Annotation,
383 RuntimeKind::Value => SymbolKind::Variable,
384 }
385}
386
387fn extract_exports(program: &Program) -> Vec<ExportedSymbol> {
389 shape_runtime::module_loader::collect_exported_symbols(program)
390 .unwrap_or_default()
391 .into_iter()
392 .map(|sym| ExportedSymbol {
393 name: sym.name,
394 alias: sym.alias,
395 kind: map_module_export_kind(sym.kind),
396 span: sym.span,
397 })
398 .collect()
399}
400
401#[cfg(test)]
402mod tests {
403 use super::*;
404
405 #[test]
406 fn test_resolve_stdlib_import() {
407 let cache = ModuleCache::new();
408 let current_file =
409 PathBuf::from("/home/dev/dev/finance/analysis-suite/shape/examples/test.shape");
410
411 let resolved = cache.resolve_import("std::core::math", ¤t_file, None);
412
413 assert!(resolved.is_some());
414 let path = resolved.unwrap();
415 let path_str = path.to_string_lossy();
416 assert!(
417 path_str.contains("stdlib/core/math.shape")
418 || path_str.contains("stdlib-src/core/math.shape"),
419 "Expected stdlib math path, got: {}",
420 path_str
421 );
422 }
423
424 #[test]
425 fn test_relative_import_is_supported() {
426 let tmp = tempfile::tempdir().unwrap();
427 let current_file = tmp.path().join("main.shape");
428 let util = tmp.path().join("utils.shape");
429 std::fs::write(¤t_file, "from ./utils use { helper }").unwrap();
430 std::fs::write(&util, "pub fn helper() { 1 }").unwrap();
431
432 let cache = ModuleCache::new();
433
434 let resolved = cache.resolve_import("./utils", ¤t_file, None);
435 assert_eq!(resolved.as_deref(), Some(util.as_path()));
436 }
437
438 #[test]
439 fn test_non_std_import_returns_none() {
440 let cache = ModuleCache::new();
441 let current_file = PathBuf::from("/home/user/project/src/main.shape");
442
443 let resolved = cache.resolve_import("finance::indicators", ¤t_file, None);
445 assert!(resolved.is_none());
446 }
447
448 #[test]
449 fn test_exported_symbol_name() {
450 let symbol = ExportedSymbol {
451 name: "originalName".to_string(),
452 alias: Some("aliasName".to_string()),
453 kind: SymbolKind::Function,
454 span: Span::default(),
455 };
456
457 assert_eq!(symbol.exported_name(), "aliasName");
458
459 let symbol_no_alias = ExportedSymbol {
460 name: "originalName".to_string(),
461 alias: None,
462 kind: SymbolKind::Function,
463 span: Span::default(),
464 };
465
466 assert_eq!(symbol_no_alias.exported_name(), "originalName");
467 }
468
469 #[test]
470 fn test_extract_exports() {
471 let source = r#"
472pub fn myFunc(x) {
473 return x + 1;
474}
475
476fn localFunc() {
477 return 42;
478}
479"#;
480
481 let program = parse_program(source).unwrap();
482 let exports = extract_exports(&program);
483
484 assert_eq!(exports.len(), 1);
485 assert_eq!(exports[0].name, "myFunc");
486 assert_eq!(exports[0].kind, SymbolKind::Function);
487 }
488
489 #[test]
490 fn test_list_stdlib_modules_not_empty() {
491 let cache = ModuleCache::new();
492 let modules = cache.list_stdlib_modules();
493 assert!(
494 !modules.is_empty(),
495 "expected stdlib module list to be non-empty"
496 );
497 assert!(
498 modules.iter().all(|m| m.starts_with("std::")),
499 "all stdlib modules should be std::-prefixed: {:?}",
500 modules
501 );
502 }
503
504 #[test]
505 fn test_list_stdlib_children_for_std_prefix() {
506 let cache = ModuleCache::new();
507 let children = cache.list_stdlib_children("std");
508 assert!(
509 !children.is_empty(),
510 "expected stdlib root to have child modules"
511 );
512 assert!(
513 children.iter().any(|c| c.name == "core"),
514 "expected std.core child in stdlib tree"
515 );
516 }
517
518 #[test]
519 fn test_list_importable_modules_with_project_modules_and_deps() {
520 let tmp = tempfile::tempdir().unwrap();
521 let root = tmp.path();
522 std::fs::write(
523 root.join("shape.toml"),
524 r#"
525[modules]
526paths = ["lib"]
527
528[dependencies]
529mydep = { path = "deps/mydep" }
530"#,
531 )
532 .unwrap();
533
534 std::fs::create_dir_all(root.join("src")).unwrap();
535 std::fs::create_dir_all(root.join("lib")).unwrap();
536 std::fs::create_dir_all(root.join("deps/mydep")).unwrap();
537
538 std::fs::write(root.join("src/main.shape"), "let x = 1").unwrap();
539 std::fs::write(root.join("lib/tools.shape"), "pub fn tool() { 1 }").unwrap();
540 std::fs::write(root.join("deps/mydep/index.shape"), "pub fn root() { 1 }").unwrap();
541 std::fs::write(root.join("deps/mydep/util.shape"), "pub fn util() { 1 }").unwrap();
542
543 let cache = ModuleCache::new();
544 let modules =
545 cache.list_importable_modules_with_context(&root.join("src/main.shape"), None);
546
547 assert!(
548 modules.iter().any(|m| m == "tools"),
549 "expected module path from [modules].paths, got: {:?}",
550 modules
551 );
552 assert!(
553 modules.iter().any(|m| m == "mydep"),
554 "expected dependency index module path, got: {:?}",
555 modules
556 );
557 assert!(
558 modules.iter().any(|m| m == "mydep::util"),
559 "expected dependency submodule path, got: {:?}",
560 modules
561 );
562 }
563
564 #[test]
565 fn test_module_cache_invalidation() {
566 let cache = ModuleCache::new();
567 let path = PathBuf::from("/test/module.shape");
568
569 let program = Arc::new(Program {
571 items: vec![],
572 docs: shape_ast::ast::ProgramDocs::default(),
573 });
574 let module_info = ModuleInfo {
575 path: path.clone(),
576 program,
577 exports: vec![],
578 };
579
580 cache.modules.insert(path.clone(), module_info.clone());
582 assert!(cache.get_module(&path).is_some());
583
584 cache.invalidate(&path);
586 assert!(cache.get_module(&path).is_none());
587 }
588}