1use std::collections::BTreeMap;
29use std::fs;
30use std::io;
31use std::path::{Component, Path, PathBuf};
32
33use ridl_ir::codegen::{header_control_character, normalise_header};
34use ridl_syntax::ast::{AstNode as _, SourceFile};
35use rowan::{TextRange, TextSize};
36
37use crate::db::{InputFile, RidlDatabase, parse_file};
38use crate::diag::{DiagCode, Diagnostic, FileId, Severity, SourceMap, Span};
39use crate::interface_lock;
40use crate::lint::{LintLevels, LintScopes};
41use crate::manifest::{Manifest, ManifestKind, TimingDefaults, parse_manifest};
42use crate::package::{Package, PackageLock, PackageOrigin, Workspace, package_declarations};
43
44pub struct LoadedWorkspace {
53 pub workspace: Workspace,
54 pub diagnostics: Vec<Diagnostic>,
55 pub sources: SourceMap,
56 pub lints: LintScopes,
57 pub codegen_header: Option<String>,
62 pub report_scope: Option<PathBuf>,
71 pub units: BTreeMap<String, PathBuf>,
76}
77
78#[derive(Clone, Debug, PartialEq, Eq)]
80pub struct Overlay {
81 pub path: PathBuf,
82 pub text: String,
83}
84
85#[derive(Debug)]
87pub enum LoadError {
88 Io(io::Error),
89 OverlayNotSource(PathBuf),
90 OverlayOutsideWorkspace {
91 path: PathBuf,
92 missing_directory: bool,
93 },
94}
95
96impl std::fmt::Display for LoadError {
97 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98 match self {
99 Self::Io(e) => write!(f, "{e}"),
100 Self::OverlayNotSource(p) => write!(
101 f,
102 "overlay `{}` is not a `.typl`, `.ridl` or `.rsdl` file",
103 p.display()
104 ),
105 Self::OverlayOutsideWorkspace {
106 path,
107 missing_directory: true,
108 } => write!(
109 f,
110 "overlay `{}` is in a directory that does not exist; create the directory first",
111 path.display()
112 ),
113 Self::OverlayOutsideWorkspace {
114 path,
115 missing_directory: false,
116 } => write!(
117 f,
118 "overlay `{}` is not in a package directory of the workspace loaded from this path",
119 path.display()
120 ),
121 }
122 }
123}
124
125impl std::error::Error for LoadError {
126 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
127 match self {
128 Self::Io(e) => Some(e),
129 _ => None,
130 }
131 }
132}
133
134impl From<io::Error> for LoadError {
135 fn from(e: io::Error) -> Self {
136 Self::Io(e)
137 }
138}
139
140fn overlay_key(path: &Path) -> Option<PathBuf> {
143 let absolute = if path.is_absolute() {
144 path.to_path_buf()
145 } else {
146 std::env::current_dir().ok()?.join(path)
147 };
148 Some(
149 absolute
150 .parent()?
151 .canonicalize()
152 .ok()?
153 .join(absolute.file_name()?),
154 )
155}
156
157pub fn load_workspace(db: &mut RidlDatabase, entry: &Path) -> io::Result<LoadedWorkspace> {
181 load_workspace_with(db, entry, &[]).map_err(|error| match error {
182 LoadError::Io(e) => e,
183 _ => unreachable!("no overlay error is possible without overlays"),
184 })
185}
186
187pub fn load_workspace_with(
189 db: &mut RidlDatabase,
190 entry: &Path,
191 overlays: &[Overlay],
192) -> Result<LoadedWorkspace, LoadError> {
193 let mut loader = Loader::default();
194 for overlay in overlays {
195 if !overlay
196 .path
197 .extension()
198 .is_some_and(|ext| ext == "typl" || ext == "ridl" || ext == "rsdl")
199 {
200 return Err(LoadError::OverlayNotSource(overlay.path.clone()));
201 }
202 let key = overlay_key(&overlay.path).ok_or_else(|| LoadError::OverlayOutsideWorkspace {
203 path: overlay.path.clone(),
204 missing_directory: true,
205 })?;
206 loader.overlays.push((key, overlay.clone(), false));
207 }
208
209 if entry.is_file() {
210 match entry.parent().and_then(find_root) {
211 Some(root) => loader.load_root(db, &root)?,
212 None => loader.load_single_file(db, entry)?,
213 }
214 } else if entry.is_dir() {
215 match find_root(entry) {
216 Some(root) => loader.load_root(db, &root)?,
217 None => {
218 return Err(io::Error::new(
219 io::ErrorKind::NotFound,
220 format!("no `ridl.toml` found at or above `{}`", entry.display()),
221 )
222 .into());
223 }
224 }
225 } else {
226 return Err(io::Error::new(
227 io::ErrorKind::NotFound,
228 format!("`{}` does not exist", entry.display()),
229 )
230 .into());
231 }
232
233 if let Some((_, overlay, _)) = loader.overlays.iter().find(|(_, _, consumed)| !consumed) {
234 return Err(LoadError::OverlayOutsideWorkspace {
235 path: overlay.path.clone(),
236 missing_directory: false,
237 });
238 }
239 let report_scope = absolute(entry).and_then(|entry| {
240 loader
241 .member_dirs
242 .iter()
243 .find(|member| absolute(member).is_some_and(|member| entry.starts_with(member)))
244 .cloned()
245 });
246 let workspace = Workspace::new(&*db, loader.packages, loader.workspace_imports);
247 Ok(LoadedWorkspace {
248 workspace,
249 diagnostics: loader.diagnostics,
250 sources: loader.sources,
251 lints: loader.lints,
252 codegen_header: loader.codegen_header,
253 report_scope,
254 units: loader.units,
255 })
256}
257
258pub fn find_root(dir: &Path) -> Option<PathBuf> {
278 let package = dir
279 .ancestors()
280 .find(|candidate| candidate.join("ridl.toml").is_file())?
281 .to_path_buf();
282 if !matches!(
283 read_manifest_kind(&package),
284 Some(ManifestKind::Package { .. })
285 ) {
286 return Some(package);
287 }
288 let Some(absolute_package) = absolute(&package) else {
289 return Some(package);
290 };
291 for (levels, parent) in absolute_package.ancestors().skip(1).enumerate() {
292 if parent.join("ridl.toml").is_file() {
293 match read_manifest_kind(parent) {
294 None => return Some(up(&package, levels + 1)),
295 Some(ManifestKind::Workspace { members }) => {
296 let listed = members
297 .iter()
298 .any(|member| normalize(&parent.join(member)) == absolute_package);
299 return Some(if listed {
300 up(&package, levels + 1)
301 } else {
302 package
303 });
304 }
305 Some(ManifestKind::Package { .. }) => {}
306 }
307 }
308 if parent.join(".git").exists() {
309 break;
310 }
311 }
312 Some(package)
313}
314
315fn read_manifest_kind(dir: &Path) -> Option<ManifestKind> {
318 let text = fs::read_to_string(dir.join("ridl.toml")).ok()?;
319 parse_manifest(FileId::DETACHED, &text)
320 .0
321 .map(|manifest| manifest.kind)
322}
323
324fn absolute(path: &Path) -> Option<PathBuf> {
327 if path.is_absolute() {
328 Some(normalize(path))
329 } else {
330 Some(normalize(&std::env::current_dir().ok()?.join(path)))
331 }
332}
333
334fn normalize(path: &Path) -> PathBuf {
337 let mut normalized = PathBuf::new();
338 for component in path.components() {
339 match component {
340 Component::CurDir => {}
341 Component::ParentDir => {
342 if matches!(
343 normalized.components().next_back(),
344 Some(Component::Normal(_))
345 ) {
346 normalized.pop();
347 } else if !normalized.has_root() {
348 normalized.push("..");
349 }
350 }
351 other => normalized.push(other),
352 }
353 }
354 normalized
355}
356
357fn up(path: &Path, levels: usize) -> PathBuf {
364 let mut result = path.to_path_buf();
365 for _ in 0..levels {
366 match result.components().next_back() {
367 Some(Component::Normal(_)) => {
368 result.pop();
369 if result.as_os_str().is_empty() {
370 result = PathBuf::from(".");
371 }
372 }
373 Some(Component::RootDir | Component::Prefix(_)) => {}
374 Some(Component::CurDir) | None => result = PathBuf::from(".."),
375 Some(Component::ParentDir) => result.push(".."),
376 }
377 }
378 result
379}
380
381type LoadedFile = (InputFile, Vec<(String, TextRange)>);
384
385#[derive(Default)]
387struct Loader {
388 claims: BTreeMap<String, (String, PathBuf)>,
391 overlays: Vec<(PathBuf, Overlay, bool)>,
392 sources: SourceMap,
393 diagnostics: Vec<Diagnostic>,
394 packages: Vec<Package>,
395 workspace_imports: BTreeMap<String, String>,
398 workspace_defaults: TimingDefaults,
403 workspace_lints: LintLevels,
408 lints: LintScopes,
413 codegen_header: Option<String>,
415 member_dirs: Vec<PathBuf>,
418 units: BTreeMap<String, PathBuf>,
420}
421
422impl Loader {
423 fn load_root(&mut self, db: &mut RidlDatabase, root: &Path) -> io::Result<()> {
426 let manifest_path = root.join("ridl.toml");
427 let text = fs::read_to_string(&manifest_path).map_err(|e| {
430 io::Error::new(
431 e.kind(),
432 format!("cannot read `{}`: {e}", manifest_path.display()),
433 )
434 })?;
435 let file_id = self.sources.file_id(&path_string(&manifest_path), &text);
436 let (manifest, diags) = parse_manifest(file_id, &text);
437 self.diagnostics.extend(diags);
438 let Some(Manifest {
439 kind,
440 imports,
441 defaults,
442 lints,
443 codegen_header_file,
444 }) = manifest
445 else {
446 return Ok(());
447 };
448 if let Some((relative, range)) = codegen_header_file {
449 let path = root.join(&relative);
450 match fs::read_to_string(&path) {
451 Ok(header) => match header_control_character(&header) {
452 None => self.codegen_header = normalise_header(&header),
453 Some(c) => self.diagnostics.push(error(
454 DiagCode::MANI_011,
455 file_id,
456 byte_range(range.start, range.end),
457 format!(
458 "`[codegen] header-file` contains a control character \
459 (U+{:04X}): `{}`",
460 u32::from(c),
461 path.display()
462 ),
463 )),
464 },
465 Err(e) => self.diagnostics.push(error(
466 DiagCode::MANI_011,
467 file_id,
468 byte_range(range.start, range.end),
469 format!(
470 "`[codegen] header-file` cannot be read: `{}`: {e}",
471 path.display()
472 ),
473 )),
474 }
475 }
476 let mut root_lints = LintLevels::default();
480 root_lints.overlay(&lints);
481 self.lints.insert(root.to_path_buf(), root_lints.clone());
482 match kind {
483 ManifestKind::Package { name, .. } => {
484 self.units.insert(name.clone(), root.to_path_buf());
488 let lock = self.read_lock(root)?;
489 self.load_package_tree(db, root, root, &name, &name, &imports, &defaults, &lock)?;
490 }
491 ManifestKind::Workspace { members } => {
492 self.workspace_imports = imports;
496 self.workspace_defaults = defaults;
497 self.workspace_lints = root_lints;
498 for member in &members {
499 self.member_dirs.push(root.join(member));
500 self.load_member(db, root, member, file_id, &text)?;
501 }
502 }
503 }
504 Ok(())
505 }
506
507 fn load_member(
511 &mut self,
512 db: &mut RidlDatabase,
513 workspace_root: &Path,
514 member: &str,
515 workspace_file: FileId,
516 workspace_text: &str,
517 ) -> io::Result<()> {
518 let manifest_path = workspace_root.join(member).join("ridl.toml");
519 if !manifest_path.is_file() {
520 self.diagnostics.push(error(
523 DiagCode::MANI_008,
524 workspace_file,
525 member_entry_range(workspace_text, member),
526 format!("workspace member `{member}` has no `ridl.toml`"),
527 ));
528 return Ok(());
529 }
530 let text = fs::read_to_string(&manifest_path)?;
531 let file_id = self.sources.file_id(&path_string(&manifest_path), &text);
532 let (manifest, diags) = parse_manifest(file_id, &text);
533 self.diagnostics.extend(diags);
534 let Some(Manifest {
535 kind,
536 imports,
537 defaults,
538 lints,
539 codegen_header_file,
540 }) = manifest
541 else {
542 return Ok(());
543 };
544 if let Some((_, range)) = codegen_header_file {
545 self.diagnostics.push(error(
546 DiagCode::MANI_012,
547 file_id,
548 byte_range(range.start, range.end),
549 format!(
550 "`[codegen] header-file` is set in workspace member `{member}`; set it in the workspace root's `ridl.toml`"
551 ),
552 ));
553 }
554 let mut member_lints = self.workspace_lints.clone();
559 member_lints.overlay(&lints);
560 self.lints.insert(workspace_root.join(member), member_lints);
561 match kind {
562 ManifestKind::Workspace { .. } => {
563 self.diagnostics.push(error(
564 DiagCode::MANI_004,
565 file_id,
566 workspace_section_range(&text),
567 format!(
568 "workspace member `{member}` declares `[workspace]`; nested workspaces are forbidden"
569 ),
570 ));
571 }
572 ManifestKind::Package { name, .. } => {
573 let member_defaults = defaults.or(&self.workspace_defaults);
579 let member_dir = workspace_root.join(member);
580 if let Some(first_dir) = self.units.get(&name)
585 && *first_dir != member_dir
586 {
587 let first_dir = first_dir.clone();
588 self.diagnostics.push(error(
589 DiagCode::MANI_014,
590 file_id,
591 package_name_range(&text),
592 format!(
593 "source package `{name}` is already declared by the unit in `{}`; the unit in `{}` declares it too. A source package belongs to one unit",
594 first_dir.display(),
595 member_dir.display()
596 ),
597 ));
598 return Ok(());
599 }
600 self.units.insert(name.clone(), member_dir.clone());
601 let lock = self.read_lock(&member_dir)?;
602 self.load_package_tree(
603 db,
604 &member_dir,
605 &member_dir,
606 &name,
607 &name,
608 &imports,
609 &member_defaults,
610 &lock,
611 )?;
612 }
613 }
614 Ok(())
615 }
616
617 #[allow(clippy::too_many_arguments)]
632 fn load_package_tree(
633 &mut self,
634 db: &mut RidlDatabase,
635 dir: &Path,
636 unit_dir: &Path,
637 unit: &str,
638 name: &str,
639 imports: &BTreeMap<String, String>,
640 defaults: &TimingDefaults,
641 lock: &Option<PackageLock>,
642 ) -> io::Result<()> {
643 let mut source_files = Vec::new();
644 let mut subdirs = Vec::new();
645 for entry in fs::read_dir(dir)? {
646 let entry = entry?;
647 let path = entry.path();
648 let is_symlink = entry.file_type()?.is_symlink();
649 if path.is_dir() {
650 if !is_symlink {
651 subdirs.push(path);
652 }
653 } else if path
654 .extension()
655 .is_some_and(|ext| ext == "typl" || ext == "ridl" || ext == "rsdl")
656 {
657 source_files.push(path);
658 }
659 }
660 if !self.overlays.is_empty() {
661 let directory_key = dir.canonicalize()?;
662 for (key, _, _) in &self.overlays {
663 if key.parent() == Some(directory_key.as_path())
664 && !source_files
665 .iter()
666 .any(|p| overlay_key(p).as_ref() == Some(key))
667 {
668 let added = dir.join(key.file_name().expect("overlay keys have a file name"));
669 if !source_files.contains(&added) {
670 source_files.push(added);
671 }
672 }
673 }
674 }
675 source_files.sort();
676 subdirs.sort();
677
678 let mut claimed_elsewhere = false;
679 if !source_files.is_empty() {
680 match self.claims.get(name) {
681 Some((first, first_dir)) if first_dir != unit_dir => {
682 claimed_elsewhere = true;
683 let manifest = unit_dir.join("ridl.toml");
684 let text = fs::read_to_string(&manifest)?;
685 let file = self.sources.file_id(&path_string(&manifest), &text);
686 self.diagnostics.push(error(
687 DiagCode::MANI_014,
688 file,
689 package_name_range(&text),
690 format!(
691 "source package `{name}` is already declared by unit `{first}` (`{}`); unit `{unit}` declares it too, in `{}`. A source package belongs to one unit",
692 first_dir.display(),
693 unit_dir.display()
694 ),
695 ));
696 }
697 _ => {
698 self.claims
699 .insert(name.to_string(), (unit.to_string(), unit_dir.to_path_buf()));
700 }
701 }
702 }
703 if !source_files.is_empty() && !claimed_elsewhere {
704 let mut files = Vec::new();
705 for path in &source_files {
706 if let Some((input, _)) = self.load_file(db, path, Some(name))? {
707 files.push(input);
708 }
709 }
710 self.packages.push(Package::new(
711 &*db,
712 name.to_string(),
713 unit.to_string(),
714 files,
715 PackageOrigin::WorkspaceMember,
716 imports.clone(),
717 defaults.clone(),
718 lock.clone(),
719 ));
720 }
721
722 for subdir in subdirs {
723 let Some(dir_name) = subdir.file_name().map(|n| n.to_string_lossy().into_owned())
724 else {
725 continue;
726 };
727 if dir_name.starts_with('.') {
728 continue;
729 }
730 let nested_manifest = subdir.join("ridl.toml");
731 if nested_manifest.is_file() {
732 let nested_text = fs::read_to_string(&nested_manifest)?;
733 let nested_id = self
734 .sources
735 .file_id(&path_string(&nested_manifest), &nested_text);
736 self.diagnostics.push(error(
737 DiagCode::MANI_013,
738 nested_id,
739 byte_range(0, 0),
740 format!(
741 "`{}` is a `ridl.toml` inside the tree of unit `{unit}`; a unit holds one manifest. Move the directory beside the unit, or delete the manifest",
742 nested_manifest.display()
743 ),
744 ));
745 continue;
746 }
747 let ignored = subdir.join(interface_lock::FILE_NAME);
748 if ignored.is_file() {
749 let ignored_text = fs::read_to_string(&ignored).unwrap_or_default();
750 let ignored_id = self.sources.file_id(&path_string(&ignored), &ignored_text);
751 self.diagnostics.push(warning(
752 DiagCode::RIDL_416,
753 ignored_id,
754 byte_range(0, 0),
755 format!(
756 "`{}` is an `interfaces.lock` inside the tree of unit `{unit}`; only the `interfaces.lock` beside the unit's `ridl.toml` is read, so this file is ignored",
757 ignored.display()
758 ),
759 ));
760 }
761 self.load_package_tree(
762 db,
763 &subdir,
764 unit_dir,
765 unit,
766 &format!("{name}.{dir_name}"),
767 imports,
768 defaults,
769 lock,
770 )?;
771 }
772 Ok(())
773 }
774
775 fn load_single_file(&mut self, db: &mut RidlDatabase, path: &Path) -> io::Result<()> {
782 let Some((input, decls)) = self.load_file(db, path, None)? else {
783 return Ok(());
785 };
786 let name = decls
787 .first()
788 .map(|(name, _)| name.clone())
789 .filter(|name| !name.is_empty())
790 .unwrap_or_else(|| {
791 path.file_stem()
792 .map(|stem| stem.to_string_lossy().into_owned())
793 .unwrap_or_else(|| "package".to_string())
794 });
795 let lock = match path.parent() {
798 Some(dir) => self.read_lock(dir)?,
799 None => None,
800 };
801 self.packages.push(Package::new(
802 &*db,
803 name.clone(),
804 name.clone(),
805 vec![input],
806 PackageOrigin::WorkspaceMember,
807 BTreeMap::new(),
808 TimingDefaults::default(),
809 lock,
810 ));
811 if let Some(dir) = path.parent() {
812 self.units.insert(name, dir.to_path_buf());
813 }
814 Ok(())
815 }
816
817 fn read_lock(&mut self, dir: &Path) -> io::Result<Option<PackageLock>> {
825 let path = path_string(&dir.join(interface_lock::FILE_NAME));
826 let text = match interface_lock::read(dir) {
827 Ok(Some(text)) => text,
828 Ok(None) => return Ok(None),
829 Err(err) if err.kind() == io::ErrorKind::InvalidData => {
830 let file_id = self.sources.file_id(&path, "");
831 self.diagnostics.push(error(
832 DiagCode::RIDL_410,
833 file_id,
834 byte_range(0, 0),
835 malformed_lock_message("the file is not valid UTF-8"),
836 ));
837 return Ok(None);
838 }
839 Err(err) => return Err(err),
840 };
841 match interface_lock::parse(&text) {
842 Ok(lock) => Ok(Some(PackageLock { path, text, lock })),
843 Err(malformed) => {
844 let file_id = self.sources.file_id(&path, &text);
845 self.diagnostics.push(error(
846 DiagCode::RIDL_410,
847 file_id,
848 malformed.range,
849 malformed_lock_message(&malformed.message),
850 ));
851 Ok(None)
852 }
853 }
854 }
855
856 fn load_file(
864 &mut self,
865 db: &mut RidlDatabase,
866 path: &Path,
867 expected: Option<&str>,
868 ) -> io::Result<Option<LoadedFile>> {
869 let path_str = path_string(path);
870 let replacement = self
871 .overlays
872 .iter_mut()
873 .filter(|(key, _, _)| overlay_key(path).as_ref() == Some(key))
874 .map(|(_, overlay, consumed)| {
875 *consumed = true;
876 overlay.text.clone()
877 })
878 .last();
879 let text = match replacement
880 .map(Ok)
881 .unwrap_or_else(|| fs::read_to_string(path))
882 {
883 Ok(text) => text,
884 Err(err) if err.kind() == io::ErrorKind::InvalidData => {
885 let file_id = self.sources.file_id(&path_str, "");
889 self.diagnostics.push(error(
890 DiagCode::NONE,
891 file_id,
892 byte_range(0, 0),
893 format!("`{path_str}` is not valid UTF-8; the file is skipped"),
894 ));
895 return Ok(None);
896 }
897 Err(err) => return Err(err),
898 };
899 let file_id = self.sources.file_id(&path_str, &text);
900 let input = InputFile::new(&*db, path_str, text);
901
902 let parse = parse_file(&*db, input);
903 let source =
904 SourceFile::cast(parse.syntax()).expect("parser roots every tree in a SourceFile");
905 let decls = package_declarations(&source);
906
907 for (_, range) in decls.iter().skip(1) {
908 self.diagnostics.push(error(
909 DiagCode::TYPL_001,
910 file_id,
911 *range,
912 "more than one `package` declaration in this file".to_string(),
913 ));
914 }
915 if let Some((declared, range)) = decls.first()
916 && crate::std_lib::is_reserved_package_name(declared)
917 {
918 self.diagnostics.push(error(
932 DiagCode::TYPL_010,
933 file_id,
934 *range,
935 format!(
936 "`{declared}` is provided by the compiler, so a package cannot declare it; every package already imports all of `{declared}` implicitly (typl §3.2), which leaves these declarations unreachable under their own name. Rename the package"
937 ),
938 ));
939 }
940 if let (Some(expected), Some((declared, range))) = (expected, decls.first())
941 && !declared.is_empty()
942 && declared != expected
943 {
944 self.diagnostics.push(error(
945 DiagCode::TYPL_002,
946 file_id,
947 *range,
948 format!(
949 "package name `{declared}` does not mirror the directory path; every file in this directory must declare `package {expected}`"
950 ),
951 ));
952 }
953 Ok(Some((input, decls)))
954 }
955}
956
957fn path_string(path: &Path) -> String {
959 path.to_string_lossy().into_owned()
960}
961
962fn malformed_lock_message(reason: &str) -> String {
965 format!(
966 "`{}` is malformed: {reason} — resolve the conflict or restore the file from version \
967 control, then run `ridl lock`",
968 interface_lock::FILE_NAME
969 )
970}
971
972fn member_entry_range(text: &str, member: &str) -> TextRange {
976 let quoted = format!("\"{member}\"");
977 match text.find("ed) {
978 Some(start) => byte_range(start, start + quoted.len()),
979 None => byte_range(0, text.len()),
980 }
981}
982
983fn package_name_range(text: &str) -> TextRange {
986 let mut offset = 0;
987 for line in text.split_inclusive('\n') {
988 let value = line
989 .trim_start()
990 .strip_prefix("name")
991 .and_then(|rest| rest.trim_start().strip_prefix('='))
992 .map(str::trim_start)
993 .and_then(|value| value.strip_prefix('"').map(|inner| (value, inner)));
994 if let Some((value, inner)) = value
995 && let Some(len) = inner.find('"')
996 {
997 let start = offset + (line.len() - value.len());
998 return byte_range(start, start + len + 2);
999 }
1000 offset += line.len();
1001 }
1002 byte_range(0, text.len())
1003}
1004
1005fn workspace_section_range(text: &str) -> TextRange {
1008 const HEADER: &str = "[workspace]";
1009 match text.find(HEADER) {
1010 Some(start) => byte_range(start, start + HEADER.len()),
1011 None => byte_range(0, text.len()),
1012 }
1013}
1014
1015fn byte_range(start: usize, end: usize) -> TextRange {
1017 TextRange::new(TextSize::from(start as u32), TextSize::from(end as u32))
1018}
1019
1020fn error(code: DiagCode, file: FileId, range: TextRange, message: String) -> Diagnostic {
1023 Diagnostic {
1024 code,
1025 severity: Severity::Error,
1026 message,
1027 primary: Span { file, range },
1028 labels: Vec::new(),
1029 fixits: Vec::new(),
1030 }
1031}
1032
1033fn warning(code: DiagCode, file: FileId, range: TextRange, message: String) -> Diagnostic {
1034 Diagnostic {
1035 severity: Severity::Warning,
1036 ..error(code, file, range, message)
1037 }
1038}
1039
1040#[cfg(test)]
1041mod tests {
1042 use std::path::PathBuf;
1043 use std::sync::atomic::{AtomicUsize, Ordering};
1044
1045 use salsa::Setter;
1046 use salsa::plumbing::AsId;
1047
1048 use super::*;
1049 use crate::lint::{LintLevel, apply_lint_levels, lint_by_name};
1050
1051 struct TempDir(PathBuf);
1053
1054 impl TempDir {
1055 fn new(label: &str) -> Self {
1056 static COUNTER: AtomicUsize = AtomicUsize::new(0);
1057 let mut path = std::env::temp_dir();
1058 path.push(format!(
1059 "ridl-core-workspace-{label}-{}-{}",
1060 std::process::id(),
1061 COUNTER.fetch_add(1, Ordering::SeqCst),
1062 ));
1063 fs::create_dir_all(&path).expect("create the temp dir");
1064 Self(path)
1065 }
1066
1067 fn path(&self) -> &Path {
1068 &self.0
1069 }
1070
1071 fn write(&self, relative: &str, text: &str) -> PathBuf {
1073 let path = self.0.join(relative);
1074 fs::create_dir_all(path.parent().expect("relative paths have a parent"))
1075 .expect("create parent directories");
1076 fs::write(&path, text).expect("write the fixture file");
1077 path
1078 }
1079 }
1080
1081 impl Drop for TempDir {
1082 fn drop(&mut self) {
1083 let _ = fs::remove_dir_all(&self.0);
1084 }
1085 }
1086
1087 fn codes(diags: &[Diagnostic]) -> Vec<&str> {
1088 diags.iter().map(|d| d.code.as_str()).collect()
1089 }
1090
1091 fn overlay_fixture() -> (TempDir, PathBuf) {
1092 let dir = TempDir::new("overlay");
1093 dir.write(
1094 "p/ridl.toml",
1095 "[package]\nname = \"p\"\nversion = \"1.0.0\"\n",
1096 );
1097 let path = dir.write("p/a.typl", "package p\ntype A: integer [0..1]\n");
1098 (dir, path)
1099 }
1100
1101 fn overlay(path: PathBuf, text: &str) -> Overlay {
1102 Overlay {
1103 path,
1104 text: text.to_string(),
1105 }
1106 }
1107
1108 #[test]
1109 fn overlay_replaces_the_text_of_a_file_on_disk() {
1110 let (dir, path) = overlay_fixture();
1111 let text = "package p\n\ntype Other: integer [0..1]\n";
1112 let mut db = RidlDatabase::default();
1113 let loaded = load_workspace_with(
1114 &mut db,
1115 &dir.path().join("p"),
1116 &[overlay(path.clone(), text)],
1117 )
1118 .unwrap();
1119 let files = loaded.workspace.packages(&db)[0].files(&db);
1120 assert_eq!(files.len(), 1);
1121 let file = files[0];
1122 assert_eq!(file.text(&db), text);
1123 let entries: Vec<_> = loaded.sources.iter_files().collect();
1124 assert!(entries.contains(&(path_string(&path).as_str(), text)));
1125 }
1126
1127 #[test]
1128 fn an_added_file_sorts_with_the_disk_files() {
1129 let (dir, _) = overlay_fixture();
1130 let mut db = RidlDatabase::default();
1131 let loaded = load_workspace_with(
1132 &mut db,
1133 &dir.path().join("p"),
1134 &[overlay(dir.path().join("p/0.typl"), "package p\n")],
1135 )
1136 .unwrap();
1137 let files = loaded.workspace.packages(&db)[0].files(&db);
1138 assert_eq!(files.len(), 2);
1139 assert!(files[0].path(&db).ends_with("0.typl"));
1140 assert!(files[1].path(&db).ends_with("a.typl"));
1141 }
1142 #[test]
1143 fn an_overlay_replaces_rather_than_adds() {
1144 let (dir, path) = overlay_fixture();
1145 let mut db = RidlDatabase::default();
1146 let loaded = load_workspace_with(
1147 &mut db,
1148 &dir.path().join("p"),
1149 &[overlay(
1150 path,
1151 "package p\ntype Replacement: integer [0..1]\n",
1152 )],
1153 )
1154 .unwrap();
1155 assert_eq!(loaded.workspace.packages(&db)[0].files(&db).len(), 1);
1156 }
1157
1158 #[test]
1159 fn overlay_adds_a_file_to_the_package_of_its_directory() {
1160 let (dir, _) = overlay_fixture();
1161 let mut db = RidlDatabase::default();
1162 let loaded = load_workspace_with(
1163 &mut db,
1164 &dir.path().join("p"),
1165 &[overlay(dir.path().join("p/b.typl"), "package p\n")],
1166 )
1167 .unwrap();
1168 let files = loaded.workspace.packages(&db)[0].files(&db);
1169 assert_eq!(files.len(), 2);
1170 assert!(files[0].path(&db).ends_with("a.typl"));
1171 assert!(files[1].path(&db).ends_with("b.typl"));
1172 }
1173
1174 #[test]
1175 fn overlay_in_an_existing_subdirectory_joins_its_package() {
1176 let (dir, _) = overlay_fixture();
1177 dir.write("p/sub/c.typl", "package p.sub\n");
1178 let mut db = RidlDatabase::default();
1179 let loaded = load_workspace_with(
1180 &mut db,
1181 &dir.path().join("p"),
1182 &[overlay(dir.path().join("p/sub/d.typl"), "package p.sub\n")],
1183 )
1184 .unwrap();
1185 let package = loaded
1186 .workspace
1187 .packages(&db)
1188 .iter()
1189 .find(|p| p.name(&db) == "p.sub")
1190 .unwrap();
1191 let files = package.files(&db);
1192 assert_eq!(files.len(), 2);
1193 assert!(files[0].path(&db).ends_with("c.typl"));
1194 assert!(files[1].path(&db).ends_with("d.typl"));
1195 }
1196
1197 #[test]
1198 fn an_added_file_with_the_wrong_package_name_draws_typl_002() {
1199 let (dir, _) = overlay_fixture();
1200 let mut db = RidlDatabase::default();
1201 let loaded = load_workspace_with(
1202 &mut db,
1203 &dir.path().join("p"),
1204 &[overlay(dir.path().join("p/b.typl"), "package q\n")],
1205 )
1206 .unwrap();
1207 let diag = loaded
1208 .diagnostics
1209 .iter()
1210 .find(|d| d.code == DiagCode::TYPL_002)
1211 .unwrap();
1212 assert!(
1213 loaded
1214 .sources
1215 .path(diag.primary.file)
1216 .unwrap()
1217 .ends_with("b.typl")
1218 );
1219 }
1220
1221 #[test]
1222 fn an_overlay_in_a_missing_directory_is_refused() {
1223 let (dir, _) = overlay_fixture();
1224 let result = load_workspace_with(
1225 &mut RidlDatabase::default(),
1226 &dir.path().join("p"),
1227 &[overlay(
1228 dir.path().join("p/nope/e.typl"),
1229 "package p.nope\n",
1230 )],
1231 );
1232 assert!(matches!(
1233 result,
1234 Err(LoadError::OverlayOutsideWorkspace {
1235 missing_directory: true,
1236 ..
1237 })
1238 ));
1239 }
1240
1241 #[test]
1242 fn an_overlay_outside_the_workspace_is_refused() {
1243 let (dir, _) = overlay_fixture();
1244 let path = dir.write("sibling/a.typl", "package sibling\n");
1245 let result = load_workspace_with(
1246 &mut RidlDatabase::default(),
1247 &dir.path().join("p"),
1248 &[overlay(path, "package sibling\n")],
1249 );
1250 assert!(matches!(
1251 result,
1252 Err(LoadError::OverlayOutsideWorkspace {
1253 missing_directory: false,
1254 ..
1255 })
1256 ));
1257 }
1258
1259 #[test]
1260 fn an_overlay_under_a_hidden_directory_is_refused() {
1261 let (dir, _) = overlay_fixture();
1262 fs::create_dir(dir.path().join("p/.hidden")).unwrap();
1263 let result = load_workspace_with(
1264 &mut RidlDatabase::default(),
1265 &dir.path().join("p"),
1266 &[overlay(
1267 dir.path().join("p/.hidden/f.typl"),
1268 "package p.hidden\n",
1269 )],
1270 );
1271 assert!(matches!(
1272 result,
1273 Err(LoadError::OverlayOutsideWorkspace {
1274 missing_directory: false,
1275 ..
1276 })
1277 ));
1278 }
1279
1280 #[test]
1281 fn a_non_source_overlay_is_refused() {
1282 let (dir, _) = overlay_fixture();
1283 let result = load_workspace_with(
1284 &mut RidlDatabase::default(),
1285 &dir.path().join("p"),
1286 &[overlay(dir.path().join("p/ridl.toml"), "")],
1287 );
1288 assert!(matches!(result, Err(LoadError::OverlayNotSource(_))));
1289 }
1290
1291 #[test]
1292 fn single_file_mode_takes_an_overlay_for_the_entry() {
1293 let dir = TempDir::new("overlay-single");
1294 let path = dir.write("x.typl", "package x\n");
1295 let text = "package x\ntype Other: integer [0..1]\n";
1296 let mut db = RidlDatabase::default();
1297 let loaded = load_workspace_with(&mut db, &path, &[overlay(path.clone(), text)]).unwrap();
1298 assert_eq!(
1299 loaded.workspace.packages(&db)[0].files(&db)[0].text(&db),
1300 text
1301 );
1302 let result = load_workspace_with(
1303 &mut db,
1304 &path,
1305 &[overlay(dir.path().join("y.typl"), "package y\n")],
1306 );
1307 assert!(matches!(
1308 result,
1309 Err(LoadError::OverlayOutsideWorkspace {
1310 missing_directory: false,
1311 ..
1312 })
1313 ));
1314 }
1315
1316 #[cfg(unix)]
1317 #[test]
1318 fn overlay_matches_a_file_named_by_a_relative_entry() {
1319 let (dir, path) = overlay_fixture();
1320 let cwd = std::env::current_dir().unwrap();
1321 let mut relative = PathBuf::new();
1322 for part in cwd.components() {
1323 if matches!(part, std::path::Component::Normal(_)) {
1324 relative.push("..");
1325 }
1326 }
1327 relative.push(path.strip_prefix("/").unwrap());
1328 for (entry, overlay_path) in [(&relative, &path), (&path, &relative)] {
1329 let mut db = RidlDatabase::default();
1330 let text = "package p\ntype Other: integer [0..1]\n";
1331 let loaded =
1332 load_workspace_with(&mut db, entry, &[overlay(overlay_path.clone(), text)])
1333 .unwrap();
1334 assert_eq!(
1335 loaded.workspace.packages(&db)[0].files(&db)[0].text(&db),
1336 text
1337 );
1338 }
1339 drop(dir);
1340 }
1341
1342 #[test]
1343 fn load_workspace_without_overlays_is_unchanged() {
1344 let (dir, _) = overlay_fixture();
1345 let mut db = RidlDatabase::default();
1346 let first = load_workspace_with(&mut db, &dir.path().join("p"), &[]).unwrap();
1347 let describe = |db: &RidlDatabase, loaded: &LoadedWorkspace| {
1348 loaded
1349 .workspace
1350 .packages(db)
1351 .iter()
1352 .map(|p| {
1353 (
1354 p.name(db).clone(),
1355 p.files(db)
1356 .iter()
1357 .map(|f| f.path(db).clone())
1358 .collect::<Vec<_>>(),
1359 )
1360 })
1361 .collect::<Vec<_>>()
1362 };
1363 let expected = describe(&db, &first);
1364 let mut other = RidlDatabase::default();
1365 let second = load_workspace(&mut other, &dir.path().join("p")).unwrap();
1366 assert_eq!(expected, describe(&other, &second));
1367 }
1368
1369 const PACKAGE_MANIFEST: &str = "[package]\nname = \"veh.common\"\nversion = \"1.0.0\"\n";
1370
1371 #[test]
1377 fn a_package_declaring_a_reserved_name_is_refused() {
1378 const SOURCE: &str = "package ridl.std\ntype MyOwnType: m\n";
1379 let dir = TempDir::new("reserved-name");
1380 dir.write(
1381 "ridl.toml",
1382 "[package]\nname = \"ridl.std\"\nversion = \"1.0.0\"\n",
1383 );
1384 dir.write("own.typl", SOURCE);
1385
1386 let mut db = RidlDatabase::default();
1387 let loaded = load_workspace(&mut db, dir.path()).expect("the package loads");
1388 assert_eq!(
1389 codes(&loaded.diagnostics),
1390 vec!["TYPL-010"],
1391 "got: {:?}",
1392 loaded.diagnostics
1393 );
1394 let diagnostic = &loaded.diagnostics[0];
1395 assert!(
1396 diagnostic.message.contains("ridl.std"),
1397 "the message names the package: {}",
1398 diagnostic.message
1399 );
1400 assert!(
1405 diagnostic.message.contains("unreachable"),
1406 "the message states the consequence that always holds: {}",
1407 diagnostic.message
1408 );
1409 for conditional in ["artifact", "overwrit"] {
1410 assert!(
1411 !diagnostic.message.contains(conditional),
1412 "`{conditional}` is true only where the output base is the package name, \
1413 so it belongs in the catalogue entry, not the message: {}",
1414 diagnostic.message
1415 );
1416 }
1417 let declaration = SOURCE
1422 .lines()
1423 .next()
1424 .expect("the declaration is the first line");
1425 assert_eq!(
1426 (
1427 usize::from(diagnostic.primary.range.start()),
1428 usize::from(diagnostic.primary.range.end()),
1429 ),
1430 (0, declaration.len()),
1431 "reported on `{declaration}` exactly"
1432 );
1433 }
1434
1435 #[test]
1439 fn a_single_file_declaring_a_reserved_name_is_refused() {
1440 let dir = TempDir::new("reserved-name-single");
1441 let file = dir.write("ridl_std.typl", "package ridl.std\ntype MyOwnType: m\n");
1442
1443 let mut db = RidlDatabase::default();
1444 let loaded = load_workspace(&mut db, &file).expect("single-file mode loads");
1445 assert_eq!(
1446 codes(&loaded.diagnostics),
1447 vec!["TYPL-010"],
1448 "got: {:?}",
1449 loaded.diagnostics
1450 );
1451 }
1452
1453 #[test]
1456 fn only_a_compiler_provided_name_is_reserved() {
1457 let dir = TempDir::new("near-reserved");
1458 dir.write(
1459 "ridl.toml",
1460 "[package]\nname = \"ridl.stdlib\"\nversion = \"1.0.0\"\n",
1461 );
1462 dir.write("own.typl", "package ridl.stdlib\ntype MyOwnType: m\n");
1463
1464 let mut db = RidlDatabase::default();
1465 let loaded = load_workspace(&mut db, dir.path()).expect("the package loads");
1466 assert_eq!(
1467 loaded.diagnostics,
1468 Vec::new(),
1469 "`ridl.stdlib` is not a package the compiler provides"
1470 );
1471 }
1472
1473 #[test]
1477 fn two_file_package_loads_and_edit_reparses_only_the_edited_file() {
1478 let dir = TempDir::new("two-file");
1479 dir.write("ridl.toml", PACKAGE_MANIFEST);
1480 dir.write("a.typl", "package veh.common\ntype A: m\n");
1481 dir.write("b.typl", "package veh.common\ntype B: s\n");
1482
1483 let mut db = RidlDatabase::default();
1484 let loaded = load_workspace(&mut db, dir.path()).expect("the package loads");
1485 assert_eq!(
1486 loaded.diagnostics,
1487 Vec::new(),
1488 "a clean package, no diagnostics"
1489 );
1490
1491 let packages = loaded.workspace.packages(&db).clone();
1492 assert_eq!(packages.len(), 1, "one package directory, one package");
1493 assert_eq!(packages[0].name(&db).as_str(), "veh.common");
1494 assert_eq!(*packages[0].origin(&db), PackageOrigin::WorkspaceMember);
1495 assert_eq!(
1496 packages[0].imports(&db),
1497 &BTreeMap::new(),
1498 "a manifest without `[imports]` yields an empty package map",
1499 );
1500 assert_eq!(
1501 loaded.workspace.imports(&db),
1502 &BTreeMap::new(),
1503 "a standalone load leaves the workspace map empty",
1504 );
1505
1506 let files = packages[0].files(&db).clone();
1507 assert_eq!(files.len(), 2, "both .typl files load");
1508 for file in &files {
1509 assert_eq!(
1510 parse_file(&db, *file).errors(),
1511 &[],
1512 "both files parse clean"
1513 );
1514 }
1515 let a = files
1516 .iter()
1517 .copied()
1518 .find(|f| f.path(&db).ends_with("a.typl"))
1519 .expect("a.typl is loaded");
1520 let b = files
1521 .iter()
1522 .copied()
1523 .find(|f| f.path(&db).ends_with("b.typl"))
1524 .expect("b.typl is loaded");
1525
1526 db.take_executed_queries();
1529 let _ = parse_file(&db, a);
1530 let _ = parse_file(&db, b);
1531 assert_eq!(
1532 db.take_executed_queries(),
1533 Vec::new(),
1534 "re-querying unchanged inputs must run no executions",
1535 );
1536
1537 a.set_text(&mut db)
1539 .to("package veh.common\ntype A: kg\n".to_string());
1540 let _ = parse_file(&db, a);
1541 let _ = parse_file(&db, b);
1542 let executed = db.take_executed_queries();
1543 assert_eq!(
1544 executed.len(),
1545 1,
1546 "editing one file re-parses exactly one file"
1547 );
1548 assert_eq!(
1549 salsa::attach(&db, || format!("{:?}", executed[0])),
1550 format!("parse_file({:?})", a.as_id()),
1551 "the re-executed query is the parse of the edited file",
1552 );
1553 }
1554
1555 #[test]
1558 fn typl_002_on_a_mismatching_file() {
1559 let dir = TempDir::new("mismatch");
1560 dir.write("ridl.toml", PACKAGE_MANIFEST);
1561 let bad_text = "package veh.wrong\ntype B: s\n";
1562 let bad_path = dir.write("bad.typl", bad_text);
1563
1564 let mut db = RidlDatabase::default();
1565 let mut loaded = load_workspace(&mut db, dir.path()).expect("the package loads");
1566 assert_eq!(codes(&loaded.diagnostics), vec!["TYPL-002"]);
1567
1568 let diag = &loaded.diagnostics[0];
1569 assert_eq!(diag.severity, Severity::Error);
1570 assert_eq!(
1571 diag.primary.range,
1572 byte_range(0, "package veh.wrong".len()),
1573 "the primary span is the mismatching `package` line",
1574 );
1575 assert_eq!(
1576 diag.primary.file,
1577 loaded.sources.file_id(&path_string(&bad_path), bad_text),
1578 "the span points into the mismatching file",
1579 );
1580
1581 let packages = loaded.workspace.packages(&db).clone();
1583 assert_eq!(packages.len(), 1);
1584 assert_eq!(packages[0].files(&db).len(), 1);
1585 }
1586
1587 #[test]
1590 fn typl_001_on_a_double_package_declaration() {
1591 let dir = TempDir::new("double-decl");
1592 dir.write("ridl.toml", PACKAGE_MANIFEST);
1593 dir.write(
1594 "dup.typl",
1595 "package veh.common\npackage veh.extra\ntype A: m\n",
1596 );
1597
1598 let mut db = RidlDatabase::default();
1599 let loaded = load_workspace(&mut db, dir.path()).expect("the package loads");
1600 assert_eq!(codes(&loaded.diagnostics), vec!["TYPL-001"]);
1601 assert_eq!(
1602 loaded.diagnostics[0].primary.range,
1603 byte_range(19, 36),
1604 "the primary span is the second `package` declaration",
1605 );
1606 }
1607
1608 #[test]
1613 fn workspace_mode_scopes_imports_per_package() {
1614 let dir = TempDir::new("workspace");
1615 dir.write(
1616 "ridl.toml",
1617 "[workspace]\nmembers = [\"m-one\", \"m-two\"]\n\n[imports]\n\"third.dep\" = \"https://registry.example.com/third/dep@v1.0.0\"\n\"shared.util\" = \"https://registry.example.com/shared/util@v1.0.0\"\n",
1618 );
1619 dir.write(
1620 "m-one/ridl.toml",
1621 "[package]\nname = \"veh.one\"\nversion = \"1.0.0\"\n\n[imports]\n\"third.dep\" = \"https://mirror.example.com/third/dep@v2.0.0\"\n\"member.only\" = \"https://registry.example.com/member/only@v1.0.0\"\n",
1622 );
1623 dir.write("m-one/one.typl", "package veh.one\ntype A: m\n");
1624 dir.write(
1625 "m-two/ridl.toml",
1626 "[package]\nname = \"veh.two\"\nversion = \"1.0.0\"\n\n[imports]\n\"two.only\" = \"https://registry.example.com/two/only@v1.0.0\"\n",
1627 );
1628 dir.write("m-two/two.typl", "package veh.two\ntype B: s\n");
1629
1630 let mut db = RidlDatabase::default();
1631 let loaded = load_workspace(&mut db, dir.path()).expect("the workspace loads");
1632 assert_eq!(loaded.diagnostics, Vec::new(), "a clean workspace");
1633
1634 let packages = loaded.workspace.packages(&db).clone();
1635 let names: Vec<String> = packages.iter().map(|p| p.name(&db).clone()).collect();
1636 assert_eq!(
1637 names,
1638 vec!["veh.one", "veh.two"],
1639 "both members load, in member order"
1640 );
1641
1642 let workspace_imports = loaded.workspace.imports(&db).clone();
1645 assert_eq!(
1646 workspace_imports.get("third.dep").map(String::as_str),
1647 Some("https://registry.example.com/third/dep@v1.0.0"),
1648 "the workspace map keeps the root pin, not the member pin",
1649 );
1650 assert_eq!(
1651 workspace_imports.get("shared.util").map(String::as_str),
1652 Some("https://registry.example.com/shared/util@v1.0.0"),
1653 );
1654 assert_eq!(workspace_imports.len(), 2, "no member entry leaks upward");
1655
1656 let one_imports = packages[0].imports(&db).clone();
1658 assert_eq!(
1659 one_imports.get("third.dep").map(String::as_str),
1660 Some("https://mirror.example.com/third/dep@v2.0.0"),
1661 "the member's own pin shadows the workspace default for it alone",
1662 );
1663 assert_eq!(
1664 one_imports.get("member.only").map(String::as_str),
1665 Some("https://registry.example.com/member/only@v1.0.0"),
1666 );
1667 assert!(
1668 !one_imports.contains_key("two.only"),
1669 "a sibling's pin never leaks into another member",
1670 );
1671 assert_eq!(one_imports.len(), 2, "no workspace entry is merged in");
1672
1673 let two_imports = packages[1].imports(&db).clone();
1674 assert_eq!(
1675 two_imports.get("two.only").map(String::as_str),
1676 Some("https://registry.example.com/two/only@v1.0.0"),
1677 );
1678 assert!(
1679 !two_imports.contains_key("member.only"),
1680 "the sibling's pin never leaks into this member",
1681 );
1682 assert!(
1683 !two_imports.contains_key("third.dep"),
1684 "neither the root default nor the sibling's pin is merged in",
1685 );
1686 assert_eq!(two_imports.len(), 1);
1687 }
1688
1689 #[test]
1693 fn defaults_timing_precedence_package_shadows_workspace() {
1694 let dir = TempDir::new("defaults-timing");
1695 dir.write(
1696 "ridl.toml",
1697 "[workspace]\nmembers = [\"m-own\", \"m-inherit\"]\n\n[defaults]\ntiming = \"[100ms..1000ms]\"\n",
1698 );
1699 dir.write(
1701 "m-own/ridl.toml",
1702 "[package]\nname = \"veh.own\"\nversion = \"1.0.0\"\n\n[defaults]\ntiming = \"[50ms..2s]\"\n",
1703 );
1704 dir.write("m-own/own.typl", "package veh.own\ntype A: m\n");
1705 dir.write(
1707 "m-inherit/ridl.toml",
1708 "[package]\nname = \"veh.inherit\"\nversion = \"1.0.0\"\n",
1709 );
1710 dir.write("m-inherit/inherit.typl", "package veh.inherit\ntype B: s\n");
1711
1712 let mut db = RidlDatabase::default();
1713 let loaded = load_workspace(&mut db, dir.path()).expect("the workspace loads");
1714 assert_eq!(loaded.diagnostics, Vec::new(), "a clean workspace");
1715
1716 let packages = loaded.workspace.packages(&db).clone();
1717 let own = packages
1718 .iter()
1719 .find(|p| p.name(&db) == "veh.own")
1720 .expect("m-own loads");
1721 assert_eq!(
1722 own.defaults(&db).timing.as_deref(),
1723 Some("[50ms..2s]"),
1724 "the member's own `[defaults]` shadows the workspace default",
1725 );
1726 let inherit = packages
1727 .iter()
1728 .find(|p| p.name(&db) == "veh.inherit")
1729 .expect("m-inherit loads");
1730 assert_eq!(
1731 inherit.defaults(&db).timing.as_deref(),
1732 Some("[100ms..1000ms]"),
1733 "a member without `[defaults]` inherits the workspace default",
1734 );
1735 }
1736
1737 #[test]
1740 fn defaults_precedence_is_per_key() {
1741 let dir = TempDir::new("defaults-per-key");
1742 dir.write(
1743 "ridl.toml",
1744 "[workspace]\nmembers = [\"m\"]\n\n[defaults]\ncommand_timing = \"[..2s]\"\nquery_timing = \"[..4s]\"\n",
1745 );
1746 dir.write(
1747 "m/ridl.toml",
1748 "[package]\nname = \"veh.m\"\nversion = \"1.0.0\"\n\n[defaults]\nquery_timing = \"[..5s]\"\n",
1749 );
1750 dir.write("m/m.typl", "package veh.m\ntype A: m\n");
1751
1752 let mut db = RidlDatabase::default();
1753 let loaded = load_workspace(&mut db, dir.path()).expect("the workspace loads");
1754 assert_eq!(loaded.diagnostics, Vec::new(), "a clean workspace");
1755 let packages = loaded.workspace.packages(&db).clone();
1756 let member = packages
1757 .iter()
1758 .find(|p| p.name(&db) == "veh.m")
1759 .expect("the member loads");
1760 let defaults = member.defaults(&db);
1761 assert_eq!(defaults.command_timing.as_deref(), Some("[..2s]"));
1762 assert_eq!(defaults.query_timing.as_deref(), Some("[..5s]"));
1763 assert_eq!(defaults.timing, None);
1764 }
1765
1766 #[test]
1770 fn defaults_precedence_is_per_key_in_both_directions() {
1771 let dir = TempDir::new("defaults-per-key-reverse");
1772 dir.write(
1773 "ridl.toml",
1774 "[workspace]\nmembers = [\"m\"]\n\n[defaults]\ncommand_timing = \"[..2s]\"\nquery_timing = \"[..4s]\"\n",
1775 );
1776 dir.write(
1777 "m/ridl.toml",
1778 "[package]\nname = \"veh.m\"\nversion = \"1.0.0\"\n\n[defaults]\ncommand_timing = \"[..7s]\"\n",
1779 );
1780 dir.write("m/m.typl", "package veh.m\ntype A: m\n");
1781
1782 let mut db = RidlDatabase::default();
1783 let loaded = load_workspace(&mut db, dir.path()).expect("the workspace loads");
1784 assert_eq!(loaded.diagnostics, Vec::new(), "a clean workspace");
1785 let packages = loaded.workspace.packages(&db).clone();
1786 let member = packages
1787 .iter()
1788 .find(|p| p.name(&db) == "veh.m")
1789 .expect("the member loads");
1790 let defaults = member.defaults(&db);
1791 assert_eq!(
1792 defaults.command_timing.as_deref(),
1793 Some("[..7s]"),
1794 "the member's own `command_timing` wins over the workspace's",
1795 );
1796 assert_eq!(
1797 defaults.query_timing.as_deref(),
1798 Some("[..4s]"),
1799 "a member without `query_timing` inherits the workspace's",
1800 );
1801 assert_eq!(defaults.timing, None);
1802 }
1803
1804 #[test]
1807 fn standalone_rpc_defaults_ride_on_the_tree() {
1808 let dir = TempDir::new("standalone-rpc-defaults");
1809 dir.write(
1810 "ridl.toml",
1811 "[package]\nname = \"veh.common\"\nversion = \"1.0.0\"\n\n[defaults]\ncommand_timing = \"[..2s]\"\nquery_timing = \"[5ms..4s]\"\n",
1812 );
1813 dir.write("a.typl", "package veh.common\ntype A: m\n");
1814 dir.write("sub/s.typl", "package veh.common.sub\ntype S: s\n");
1815
1816 let mut db = RidlDatabase::default();
1817 let loaded = load_workspace(&mut db, dir.path()).expect("the package tree loads");
1818 assert_eq!(loaded.diagnostics, Vec::new());
1819 let packages = loaded.workspace.packages(&db).clone();
1820 let names: Vec<_> = packages.iter().map(|p| p.name(&db).clone()).collect();
1821 assert!(
1822 names.iter().any(|name| name == "veh.common")
1823 && names.iter().any(|name| name == "veh.common.sub"),
1824 "the root and the nested package both load: {names:?}",
1825 );
1826 for package in &packages {
1827 let defaults = package.defaults(&db);
1828 assert_eq!(
1829 defaults.command_timing.as_deref(),
1830 Some("[..2s]"),
1831 "{}",
1832 package.name(&db),
1833 );
1834 assert_eq!(
1835 defaults.query_timing.as_deref(),
1836 Some("[5ms..4s]"),
1837 "{}",
1838 package.name(&db),
1839 );
1840 }
1841 }
1842
1843 #[test]
1846 fn standalone_defaults_timing_rides_on_the_tree() {
1847 let dir = TempDir::new("standalone-defaults");
1848 dir.write(
1849 "ridl.toml",
1850 "[package]\nname = \"veh.common\"\nversion = \"1.0.0\"\n\n[defaults]\ntiming = \"[20ms..200ms]\"\n",
1851 );
1852 dir.write("a.typl", "package veh.common\ntype A: m\n");
1853 dir.write("sub/s.typl", "package veh.common.sub\ntype S: s\n");
1854
1855 let mut db = RidlDatabase::default();
1856 let loaded = load_workspace(&mut db, dir.path()).expect("the package tree loads");
1857 assert_eq!(loaded.diagnostics, Vec::new());
1858 for package in loaded.workspace.packages(&db) {
1859 assert_eq!(
1860 package.defaults(&db).timing.as_deref(),
1861 Some("[20ms..200ms]"),
1862 "every package in the tree carries the manifest default",
1863 );
1864 }
1865
1866 let bare = TempDir::new("standalone-defaults-bare");
1868 let single = bare.write("iface.ridl", "package solo\ntype A: m\n");
1869 let mut single_db = RidlDatabase::default();
1870 let single_loaded =
1871 load_workspace(&mut single_db, &single).expect("single-file mode loads");
1872 assert_eq!(
1873 single_loaded.workspace.packages(&single_db)[0].defaults(&single_db),
1874 &TimingDefaults::default(),
1875 "single-file mode carries no configured default",
1876 );
1877 }
1878
1879 #[test]
1886 fn lint_scopes_follow_root_then_member() {
1887 let dir = TempDir::new("lint-scopes");
1888 dir.write(
1889 "ridl.toml",
1890 "[workspace]\nmembers = [\"a\", \"b\"]\n\n[lints]\nmissing-timing = \"deny\"\nshared-error-type = \"allow\"\n",
1891 );
1892 dir.write(
1893 "a/ridl.toml",
1894 "[package]\nname = \"a\"\nversion = \"1.0.0\"\n\n[lints]\nmissing-timing = \"warn\"\n",
1895 );
1896 let a_file = dir.write("a/a.typl", "package a\ntype A: m\n");
1897 dir.write(
1898 "b/ridl.toml",
1899 "[package]\nname = \"b\"\nversion = \"1.0.0\"\n",
1900 );
1901 let b_file = dir.write("b/b.typl", "package b\ntype B: s\n");
1902
1903 let mut db = RidlDatabase::default();
1904 let loaded = load_workspace(&mut db, dir.path()).expect("the workspace loads");
1905 assert_eq!(loaded.diagnostics, Vec::new(), "a clean workspace");
1906
1907 let missing_timing = lint_by_name("missing-timing").expect("missing-timing is a lint");
1908 let shared_error_type =
1909 lint_by_name("shared-error-type").expect("shared-error-type is a lint");
1910 let levels = |path: &Path| {
1911 loaded
1912 .lints
1913 .for_path(path)
1914 .unwrap_or_else(|| panic!("`{}` is in a scope", path.display()))
1915 };
1916
1917 let in_a = levels(&a_file);
1918 assert_eq!(in_a.level(missing_timing), Some(LintLevel::Warn));
1919 assert_eq!(in_a.level(shared_error_type), Some(LintLevel::Allow));
1920 assert_eq!(levels(&b_file).level(missing_timing), Some(LintLevel::Deny));
1921 let root = levels(&dir.path().join("ridl.toml"));
1922 assert_eq!(root.level(missing_timing), Some(LintLevel::Deny));
1923 assert_eq!(root.level(shared_error_type), Some(LintLevel::Allow));
1924 assert_eq!(
1925 levels(&dir.path().join("a/ridl.toml")).level(missing_timing),
1926 Some(LintLevel::Warn),
1927 );
1928
1929 let packages = loaded.workspace.packages(&db).clone();
1932 let a = packages
1933 .iter()
1934 .find(|p| p.name(&db) == "a")
1935 .expect("member a loads");
1936 let recorded = a.files(&db)[0].path(&db).clone();
1937 assert_eq!(
1938 levels(Path::new(&recorded)).level(missing_timing),
1939 Some(LintLevel::Warn),
1940 "the recorded path `{recorded}` resolves to the member's scope",
1941 );
1942 }
1943
1944 #[test]
1948 fn a_member_manifest_diagnostic_is_in_the_member_scope() {
1949 let dir = TempDir::new("lint-scope-manifest");
1950 dir.write(
1951 "ridl.toml",
1952 "[workspace]\nmembers = [\"a\"]\n\n[lints]\nunknown-lint = \"deny\"\n",
1953 );
1954 dir.write(
1955 "a/ridl.toml",
1956 "[package]\nname = \"a\"\nversion = \"1.0.0\"\n\n[lints]\nunknown-lint = \"allow\"\nnope = \"warn\"\n",
1957 );
1958 dir.write("a/a.typl", "package a\ntype A: m\n");
1959
1960 let mut db = RidlDatabase::default();
1961 let loaded = load_workspace(&mut db, dir.path()).expect("the workspace loads");
1962 assert_eq!(codes(&loaded.diagnostics), vec!["MANI-010"]);
1963 let unknown_lint = lint_by_name("unknown-lint").expect("unknown-lint is a lint");
1964 let path = loaded
1965 .sources
1966 .path(loaded.diagnostics[0].primary.file)
1967 .expect("the manifest has a path");
1968 assert_eq!(
1969 loaded
1970 .lints
1971 .for_path(Path::new(path))
1972 .expect("the member manifest is in a scope")
1973 .level(unknown_lint),
1974 Some(LintLevel::Allow),
1975 "the recorded manifest path `{path}` resolves to the member's scope",
1976 );
1977
1978 let mut diagnostics = loaded.diagnostics.clone();
1979 apply_lint_levels(&mut diagnostics, &loaded.sources, &loaded.lints);
1980 assert_eq!(
1981 diagnostics,
1982 Vec::new(),
1983 "`unknown-lint = \"allow\"` silences the MANI-010"
1984 );
1985 }
1986
1987 #[test]
1990 fn standalone_lint_scope_covers_the_tree_and_single_file_has_none() {
1991 let dir = TempDir::new("lint-scope-standalone");
1992 dir.write(
1993 "ridl.toml",
1994 "[package]\nname = \"veh.common\"\nversion = \"1.0.0\"\n\n[lints]\nmissing-timing = \"deny\"\n",
1995 );
1996 let top = dir.write("a.typl", "package veh.common\ntype A: m\n");
1997 let nested = dir.write("sub/s.typl", "package veh.common.sub\ntype S: s\n");
1998
1999 let mut db = RidlDatabase::default();
2000 let loaded = load_workspace(&mut db, dir.path()).expect("the package tree loads");
2001 assert_eq!(loaded.diagnostics, Vec::new());
2002 let missing_timing = lint_by_name("missing-timing").expect("missing-timing is a lint");
2003 for path in [&top, &nested] {
2004 assert_eq!(
2005 loaded
2006 .lints
2007 .for_path(path)
2008 .unwrap_or_else(|| panic!("`{}` is in a scope", path.display()))
2009 .level(missing_timing),
2010 Some(LintLevel::Deny),
2011 );
2012 }
2013
2014 let bare = TempDir::new("lint-scope-bare");
2015 let single = bare.write("iface.ridl", "package solo\ntype A: m\n");
2016 let mut single_db = RidlDatabase::default();
2017 let single_loaded =
2018 load_workspace(&mut single_db, &single).expect("single-file mode loads");
2019 assert!(
2020 single_loaded.lints.for_path(&single).is_none(),
2021 "single-file mode has no lint scope",
2022 );
2023 }
2024
2025 #[test]
2028 fn mani_004_on_a_nested_workspace_member() {
2029 let dir = TempDir::new("nested");
2030 dir.write(
2031 "ridl.toml",
2032 "[workspace]\nmembers = [\"m-bad\", \"m-good\"]\n",
2033 );
2034 let bad_text = "[workspace]\nmembers = []\n";
2035 let bad_path = dir.write("m-bad/ridl.toml", bad_text);
2036 dir.write(
2037 "m-good/ridl.toml",
2038 "[package]\nname = \"veh.good\"\nversion = \"1.0.0\"\n",
2039 );
2040 dir.write("m-good/good.typl", "package veh.good\ntype A: m\n");
2041
2042 let mut db = RidlDatabase::default();
2043 let mut loaded = load_workspace(&mut db, dir.path()).expect("the workspace loads");
2044 assert_eq!(codes(&loaded.diagnostics), vec!["MANI-004"]);
2045
2046 let diag = &loaded.diagnostics[0];
2047 assert_eq!(diag.severity, Severity::Error);
2048 assert_eq!(
2049 diag.primary.file,
2050 loaded.sources.file_id(&path_string(&bad_path), bad_text),
2051 "the span points into the member's own manifest",
2052 );
2053 assert_eq!(
2054 diag.primary.range,
2055 byte_range(0, "[workspace]".len()),
2056 "the span is the `[workspace]` section header",
2057 );
2058
2059 let packages = loaded.workspace.packages(&db).clone();
2060 let names: Vec<String> = packages.iter().map(|p| p.name(&db).clone()).collect();
2061 assert_eq!(names, vec!["veh.good"], "the nested member loads nothing");
2062 }
2063
2064 #[test]
2067 fn mani_008_on_a_missing_member_directory() {
2068 let dir = TempDir::new("missing-member");
2069 dir.write(
2070 "ridl.toml",
2071 "[workspace]\nmembers = [\"m-gone\", \"m-good\"]\n",
2072 );
2073 dir.write(
2074 "m-good/ridl.toml",
2075 "[package]\nname = \"veh.good\"\nversion = \"1.0.0\"\n",
2076 );
2077 dir.write("m-good/good.typl", "package veh.good\ntype A: m\n");
2078
2079 let mut db = RidlDatabase::default();
2080 let loaded = load_workspace(&mut db, dir.path()).expect("the workspace loads");
2081 assert_eq!(codes(&loaded.diagnostics), vec!["MANI-008"]);
2082 assert_eq!(loaded.diagnostics[0].severity, Severity::Error);
2083 assert!(loaded.diagnostics[0].message.contains("m-gone"));
2084
2085 let packages = loaded.workspace.packages(&db).clone();
2086 assert_eq!(packages.len(), 1);
2087 assert_eq!(packages[0].name(&db).as_str(), "veh.good");
2088 }
2089
2090 #[test]
2094 fn a_subdirectory_is_its_own_package_named_by_its_path() {
2095 let dir = TempDir::new("subdir");
2096 dir.write(
2097 "ridl.toml",
2098 "[package]\nname = \"veh.common\"\nversion = \"1.0.0\"\n\n[imports]\n\"some.dep\" = \"https://registry.example.com/some/dep@v1.0.0\"\n",
2099 );
2100 dir.write("a.typl", "package veh.common\ntype A: m\n");
2101 dir.write("types/t.typl", "package veh.common.types\ntype T: s\n");
2102
2103 let mut db = RidlDatabase::default();
2104 let loaded = load_workspace(&mut db, dir.path()).expect("the package tree loads");
2105 assert_eq!(loaded.diagnostics, Vec::new());
2106
2107 let packages = loaded.workspace.packages(&db).clone();
2108 let names: Vec<String> = packages.iter().map(|p| p.name(&db).clone()).collect();
2109 assert_eq!(names, vec!["veh.common", "veh.common.types"]);
2110 for package in &packages {
2111 assert_eq!(
2112 package.imports(&db).get("some.dep").map(String::as_str),
2113 Some("https://registry.example.com/some/dep@v1.0.0"),
2114 "every package in the manifest's tree carries its `[imports]`",
2115 );
2116 }
2117 assert_eq!(
2118 loaded.workspace.imports(&db),
2119 &BTreeMap::new(),
2120 "a standalone load leaves the workspace map empty",
2121 );
2122 }
2123
2124 #[test]
2128 fn a_package_directory_mixes_typl_and_ridl_files() {
2129 let dir = TempDir::new("mixed");
2130 dir.write("ridl.toml", PACKAGE_MANIFEST);
2131 dir.write("a.typl", "package veh.common\ntype A: m\n");
2132 dir.write("b.ridl", "package veh.common\ntype B: s\n");
2133
2134 let mut db = RidlDatabase::default();
2135 let loaded = load_workspace(&mut db, dir.path()).expect("the package loads");
2136 assert_eq!(loaded.diagnostics, Vec::new(), "a clean mixed package");
2137
2138 let packages = loaded.workspace.packages(&db).clone();
2139 assert_eq!(packages.len(), 1, "one package directory, one package");
2140 let files = packages[0].files(&db).clone();
2141 assert_eq!(files.len(), 2, "both the .typl and the .ridl file load");
2142 for file in &files {
2143 assert_eq!(
2144 parse_file(&db, *file).errors(),
2145 &[],
2146 "both files parse clean"
2147 );
2148 }
2149 assert!(files.iter().any(|f| f.path(&db).ends_with("a.typl")));
2150 assert!(files.iter().any(|f| f.path(&db).ends_with("b.ridl")));
2151 }
2152
2153 #[test]
2157 fn a_package_directory_loads_its_rsdl_files() {
2158 let dir = TempDir::new("rsdl");
2159 dir.write("ridl.toml", PACKAGE_MANIFEST);
2160 dir.write("a.typl", "package veh.common\ntype A: m\n");
2161 dir.write("b.ridl", "package veh.common\ntype B: s\n");
2162 dir.write("c.rsdl", "package veh.common\n");
2163
2164 let mut db = RidlDatabase::default();
2165 let loaded = load_workspace(&mut db, dir.path()).expect("the package loads");
2166 assert_eq!(loaded.diagnostics, Vec::new(), "a clean mixed package");
2167
2168 let packages = loaded.workspace.packages(&db).clone();
2169 assert_eq!(packages.len(), 1, "one package directory, one package");
2170 let files = packages[0].files(&db).clone();
2171 let paths: Vec<&str> = files.iter().map(|f| f.path(&db).as_str()).collect();
2172 assert_eq!(files.len(), 3, "the .rsdl file loads too: {paths:?}");
2173 let rsdl = files
2174 .iter()
2175 .find(|f| f.path(&db).ends_with("c.rsdl"))
2176 .expect("the .rsdl file is a package file");
2177 assert_eq!(
2178 crate::db::profile_of_path(rsdl.path(&db)),
2179 ridl_syntax::Profile::Rsdl
2180 );
2181 assert_eq!(
2182 parse_file(&db, *rsdl).errors(),
2183 &[],
2184 "the .rsdl file parses clean"
2185 );
2186 }
2187
2188 #[test]
2190 fn single_file_mode_accepts_a_bare_ridl_entry() {
2191 let dir = TempDir::new("single-ridl");
2192 let path = dir.write("iface.ridl", "package veh.iface\ntype A: m\n");
2193
2194 let mut db = RidlDatabase::default();
2195 let loaded = load_workspace(&mut db, &path).expect("single-file mode loads");
2196 assert_eq!(loaded.diagnostics, Vec::new(), "exempt from TYPL-002");
2197
2198 let packages = loaded.workspace.packages(&db).clone();
2199 assert_eq!(packages.len(), 1, "one synthetic package");
2200 assert_eq!(
2201 packages[0].name(&db).as_str(),
2202 "veh.iface",
2203 "named from the file's declared package",
2204 );
2205 }
2206
2207 #[test]
2208 fn every_package_of_a_tree_carries_the_manifest_name_as_its_unit() {
2209 let dir = TempDir::new("unit-tree");
2210 dir.write(
2211 "ridl.toml",
2212 "[package]\nname = \"veh.hmi\"\nversion = \"1.0.0\"\n",
2213 );
2214 dir.write("hmi.ridl", "package veh.hmi\ntype A: m\n");
2215 dir.write("cluster/speed.ridl", "package veh.hmi.cluster\ntype S: m\n");
2216
2217 let mut db = RidlDatabase::default();
2218 let loaded = load_workspace(&mut db, dir.path()).expect("the package tree loads");
2219 assert_eq!(loaded.diagnostics, Vec::new());
2220
2221 let packages = loaded.workspace.packages(&db).clone();
2222 assert_eq!(packages.len(), 2);
2223 for package in &packages {
2224 assert_eq!(package.unit(&db).as_str(), "veh.hmi");
2225 }
2226 assert_eq!(
2227 loaded.units,
2228 BTreeMap::from([("veh.hmi".to_string(), dir.path().to_path_buf())]),
2229 );
2230 }
2231
2232 #[test]
2233 fn a_workspace_member_is_a_unit_and_the_root_is_not() {
2234 let dir = TempDir::new("unit-members");
2235 dir.write("ridl.toml", "[workspace]\nmembers = [\"a\", \"b\"]\n");
2236 dir.write(
2237 "a/ridl.toml",
2238 "[package]\nname = \"x.a\"\nversion = \"1.0.0\"\n",
2239 );
2240 dir.write("a/a.ridl", "package x.a\ntype A: m\n");
2241 dir.write(
2242 "b/ridl.toml",
2243 "[package]\nname = \"x.b\"\nversion = \"1.0.0\"\n",
2244 );
2245 dir.write("b/b.ridl", "package x.b\ntype B: m\n");
2246
2247 let mut db = RidlDatabase::default();
2248 let loaded = load_workspace(&mut db, dir.path()).expect("the workspace loads");
2249 assert_eq!(loaded.diagnostics, Vec::new());
2250
2251 assert_eq!(
2252 loaded.units,
2253 BTreeMap::from([
2254 ("x.a".to_string(), dir.path().join("a")),
2255 ("x.b".to_string(), dir.path().join("b")),
2256 ]),
2257 );
2258 let units: Vec<String> = loaded
2259 .workspace
2260 .packages(&db)
2261 .iter()
2262 .map(|p| p.unit(&db).clone())
2263 .collect();
2264 assert_eq!(units, vec!["x.a", "x.b"]);
2265 }
2266
2267 #[test]
2268 fn a_single_file_is_its_own_unit() {
2269 let dir = TempDir::new("unit-single");
2270 let path = dir.write("p.ridl", "package p\ntype A: m\n");
2271
2272 let mut db = RidlDatabase::default();
2273 let loaded = load_workspace(&mut db, &path).expect("single-file mode loads");
2274 assert_eq!(loaded.diagnostics, Vec::new());
2275
2276 let packages = loaded.workspace.packages(&db).clone();
2277 assert_eq!(packages.len(), 1);
2278 assert_eq!(packages[0].unit(&db).as_str(), "p");
2279 assert_eq!(
2280 loaded.units,
2281 BTreeMap::from([("p".to_string(), dir.path().to_path_buf())]),
2282 );
2283 }
2284
2285 #[cfg(unix)]
2288 #[test]
2289 fn a_symlinked_directory_is_not_followed() {
2290 let dir = TempDir::new("symlink");
2291 dir.write("ridl.toml", PACKAGE_MANIFEST);
2292 dir.write("a.typl", "package veh.common\ntype A: m\n");
2293 std::os::unix::fs::symlink(dir.path(), dir.path().join("loop"))
2295 .expect("create the directory symlink");
2296
2297 let mut db = RidlDatabase::default();
2298 let loaded = load_workspace(&mut db, dir.path()).expect("the package loads");
2299 assert_eq!(loaded.diagnostics, Vec::new());
2300
2301 let packages = loaded.workspace.packages(&db).clone();
2302 assert_eq!(packages.len(), 1, "the symlink cycle adds no packages");
2303 assert_eq!(packages[0].name(&db).as_str(), "veh.common");
2304 }
2305
2306 #[test]
2310 fn a_non_utf8_file_is_reported_and_skipped() {
2311 let dir = TempDir::new("non-utf8");
2312 dir.write("ridl.toml", PACKAGE_MANIFEST);
2313 dir.write("a.typl", "package veh.common\ntype A: m\n");
2314 fs::write(dir.path().join("bad.typl"), [0xFF, 0xFE, 0x00, 0x9F])
2315 .expect("write the non-UTF8 fixture");
2316
2317 let mut db = RidlDatabase::default();
2318 let loaded = load_workspace(&mut db, dir.path()).expect("the load continues");
2319 assert_eq!(loaded.diagnostics.len(), 1);
2320 assert!(
2321 loaded.diagnostics[0].message.contains("UTF-8"),
2322 "the diagnostic names the encoding problem",
2323 );
2324
2325 let packages = loaded.workspace.packages(&db).clone();
2326 assert_eq!(packages.len(), 1);
2327 let files = packages[0].files(&db).clone();
2328 assert_eq!(files.len(), 1, "only the valid file loads");
2329 assert!(files[0].path(&db).ends_with("a.typl"));
2330 }
2331
2332 #[test]
2336 fn single_file_mode_loads_the_e0_fixture() {
2337 let fixture = concat!(
2338 env!("CARGO_MANIFEST_DIR"),
2339 "/../ridl-syntax/fixtures/walking_skeleton.typl",
2340 );
2341 let text = fs::read_to_string(fixture).expect("the E0 fixture exists");
2342 assert!(
2343 text.contains("package fixtures"),
2344 "the fixture declares `package fixtures`",
2345 );
2346
2347 let dir = TempDir::new("single-file");
2351 let path = dir.write("walking_skeleton.typl", &text);
2352
2353 let mut db = RidlDatabase::default();
2354 let loaded = load_workspace(&mut db, &path).expect("single-file mode loads");
2355 assert_eq!(loaded.diagnostics, Vec::new(), "exempt from TYPL-002");
2356
2357 let packages = loaded.workspace.packages(&db).clone();
2358 assert_eq!(packages.len(), 1, "one synthetic package");
2359 assert_eq!(
2360 packages[0].name(&db).as_str(),
2361 "fixtures",
2362 "named from the file's declared package",
2363 );
2364 assert_eq!(*packages[0].origin(&db), PackageOrigin::WorkspaceMember);
2365
2366 let files = packages[0].files(&db).clone();
2367 assert_eq!(files.len(), 1);
2368 assert_eq!(
2369 parse_file(&db, files[0]).errors(),
2370 &[],
2371 "the fixture parses clean"
2372 );
2373 }
2374
2375 const LOCK_TEXT: &str = "\
2378# interfaces.lock — written by ridl lock; do not edit by hand.
2379next 3
2380Cabin 1
2381service:veh.common.climate 2
2382";
2383
2384 #[test]
2388 fn a_lock_beside_the_sources_rides_on_the_package() {
2389 let dir = TempDir::new("lock");
2390 dir.write("ridl.toml", PACKAGE_MANIFEST);
2391 dir.write("a.ridl", "package veh.common\ntype A: m\n");
2392 let lock_path = dir.write("interfaces.lock", LOCK_TEXT);
2393
2394 let mut db = RidlDatabase::default();
2395 let loaded = load_workspace(&mut db, dir.path()).expect("the package loads");
2396 assert_eq!(
2397 loaded.diagnostics,
2398 Vec::new(),
2399 "a well-formed lock draws nothing"
2400 );
2401
2402 let packages = loaded.workspace.packages(&db).clone();
2403 let lock = packages[0]
2404 .lock(&db)
2405 .as_ref()
2406 .expect("the lock rides on the package");
2407 assert_eq!(lock.path, path_string(&lock_path));
2408 assert_eq!(lock.text, LOCK_TEXT);
2409 assert_eq!(lock.lock.next, 3);
2410 assert_eq!(lock.lock.entries.len(), 2);
2411 assert_eq!(
2412 lock.lock.entries[1].key,
2413 crate::interface_lock::LockKey::Service("veh.common.climate".to_string())
2414 );
2415 }
2416
2417 #[test]
2418 fn a_package_with_no_lock_has_none() {
2419 let dir = TempDir::new("no-lock");
2420 dir.write("ridl.toml", PACKAGE_MANIFEST);
2421 dir.write("a.ridl", "package veh.common\ntype A: m\n");
2422
2423 let mut db = RidlDatabase::default();
2424 let loaded = load_workspace(&mut db, dir.path()).expect("the package loads");
2425 assert_eq!(loaded.diagnostics, Vec::new());
2426 let packages = loaded.workspace.packages(&db).clone();
2427 assert_eq!(*packages[0].lock(&db), None);
2428 }
2429
2430 #[test]
2434 fn a_malformed_lock_is_ridl_410_on_its_own_line() {
2435 let dir = TempDir::new("bad-lock");
2436 dir.write("ridl.toml", PACKAGE_MANIFEST);
2437 dir.write("a.ridl", "package veh.common\ntype A: m\n");
2438 let text = "# interfaces.lock — written by ridl lock; do not edit by hand.\n\
2439 next 2\n\
2440 Cabin 1\n\
2441 Door 1\n";
2442 let lock_path = dir.write("interfaces.lock", text);
2443
2444 let mut db = RidlDatabase::default();
2445 let loaded = load_workspace(&mut db, dir.path()).expect("the package loads");
2446 assert_eq!(codes(&loaded.diagnostics), ["RIDL-410"]);
2447 let diagnostic = &loaded.diagnostics[0];
2448 assert_eq!(diagnostic.severity, Severity::Error);
2449 assert_eq!(
2450 loaded.sources.path(diagnostic.primary.file),
2451 Some(path_string(&lock_path).as_str()),
2452 "the span is in the lock file"
2453 );
2454 assert_eq!(loaded.sources.text(diagnostic.primary.file), Some(text));
2455 let line_start = text
2456 .find("Door 1")
2457 .expect("the offending line is in the text");
2458 assert_eq!(
2459 diagnostic.primary.range,
2460 byte_range(line_start, line_start + "Door 1".len()),
2461 "the span is the offending line"
2462 );
2463 assert_eq!(
2464 diagnostic.message,
2465 "`interfaces.lock` is malformed: number 1 is on two entries: `Cabin` and `Door` — \
2466 resolve the conflict or restore the file from version control, then run `ridl lock`"
2467 );
2468
2469 let packages = loaded.workspace.packages(&db).clone();
2470 assert_eq!(
2471 *packages[0].lock(&db),
2472 None,
2473 "a malformed lock does not ride on the package"
2474 );
2475 }
2476
2477 #[test]
2480 fn an_empty_lock_is_ridl_410_at_the_start_of_the_file() {
2481 let dir = TempDir::new("empty-lock");
2482 dir.write("ridl.toml", PACKAGE_MANIFEST);
2483 dir.write("a.ridl", "package veh.common\ntype A: m\n");
2484 dir.write("interfaces.lock", "");
2485
2486 let mut db = RidlDatabase::default();
2487 let loaded = load_workspace(&mut db, dir.path()).expect("the package loads");
2488 assert_eq!(codes(&loaded.diagnostics), ["RIDL-410"]);
2489 assert_eq!(loaded.diagnostics[0].primary.range, byte_range(0, 0));
2490 assert!(
2491 loaded.diagnostics[0].message.contains("no `next` line"),
2492 "got: {}",
2493 loaded.diagnostics[0].message
2494 );
2495 }
2496
2497 #[test]
2500 fn a_lock_that_is_not_utf8_is_ridl_410() {
2501 let dir = TempDir::new("binary-lock");
2502 dir.write("ridl.toml", PACKAGE_MANIFEST);
2503 dir.write("a.ridl", "package veh.common\ntype A: m\n");
2504 fs::write(dir.path().join("interfaces.lock"), [0xff, 0xfe, b'\n'])
2505 .expect("write the bytes");
2506
2507 let mut db = RidlDatabase::default();
2508 let loaded = load_workspace(&mut db, dir.path()).expect("the package loads");
2509 assert_eq!(codes(&loaded.diagnostics), ["RIDL-410"]);
2510 assert!(
2511 loaded.diagnostics[0].message.contains("not valid UTF-8"),
2512 "got: {}",
2513 loaded.diagnostics[0].message
2514 );
2515 assert_eq!(loaded.diagnostics[0].primary.range, byte_range(0, 0));
2516 let packages = loaded.workspace.packages(&db).clone();
2517 assert_eq!(*packages[0].lock(&db), None);
2518 }
2519
2520 #[test]
2523 fn single_file_mode_reads_the_lock_beside_the_file() {
2524 let dir = TempDir::new("single-lock");
2525 let path = dir.write("iface.ridl", "package veh.iface\ntype A: m\n");
2526 dir.write("interfaces.lock", LOCK_TEXT);
2527
2528 let mut db = RidlDatabase::default();
2529 let loaded = load_workspace(&mut db, &path).expect("single-file mode loads");
2530 assert_eq!(loaded.diagnostics, Vec::new());
2531 let packages = loaded.workspace.packages(&db).clone();
2532 let lock = packages[0]
2533 .lock(&db)
2534 .as_ref()
2535 .expect("the lock rides on the synthetic package");
2536 assert_eq!(lock.lock.next, 3);
2537 }
2538
2539 #[test]
2542 fn every_package_of_a_unit_carries_the_lock_of_the_manifest_directory() {
2543 let dir = TempDir::new("unit-lock");
2544 dir.write(
2545 "ridl.toml",
2546 "[package]\nname = \"veh.hmi\"\nversion = \"1.0.0\"\n",
2547 );
2548 dir.write("hmi.ridl", "package veh.hmi\ntype A: m\n");
2549 let lock_path = dir.write("interfaces.lock", "next 1\n");
2550 dir.write("cluster/speed.ridl", "package veh.hmi.cluster\ntype B: m\n");
2551
2552 let mut db = RidlDatabase::default();
2553 let loaded = load_workspace(&mut db, dir.path()).expect("the tree loads");
2554 assert_eq!(loaded.diagnostics, Vec::new());
2555 let packages = loaded.workspace.packages(&db).clone();
2556 assert_eq!(packages.len(), 2);
2557 for package in &packages {
2558 let lock = package
2559 .lock(&db)
2560 .as_ref()
2561 .unwrap_or_else(|| panic!("`{}` has the unit's lock", package.name(&db)));
2562 assert_eq!(lock.path, path_string(&lock_path));
2563 }
2564 }
2565
2566 #[test]
2569 fn a_lock_in_a_subdirectory_is_not_read() {
2570 let dir = TempDir::new("subdir-lock-ignored");
2571 dir.write(
2572 "ridl.toml",
2573 "[package]\nname = \"veh.hmi\"\nversion = \"1.0.0\"\n",
2574 );
2575 dir.write("hmi.ridl", "package veh.hmi\ntype A: m\n");
2576 dir.write("cluster/speed.ridl", "package veh.hmi.cluster\ntype B: m\n");
2577 let ignored = dir.write("cluster/interfaces.lock", "x");
2578
2579 let mut db = RidlDatabase::default();
2580 let loaded = load_workspace(&mut db, dir.path()).expect("the tree loads");
2581 assert_eq!(codes(&loaded.diagnostics), vec!["RIDL-416"]);
2582 let diag = &loaded.diagnostics[0];
2583 assert_eq!(diag.severity, Severity::Warning);
2584 assert_eq!(
2585 loaded.sources.path(diag.primary.file),
2586 Some(path_string(&ignored).as_str())
2587 );
2588 assert_eq!(diag.primary.range, byte_range(0, 0));
2589 assert!(diag.message.contains("unit `veh.hmi`"), "{}", diag.message);
2590 let packages = loaded.workspace.packages(&db).clone();
2591 assert_eq!(packages.len(), 2);
2592 for package in &packages {
2593 assert_eq!(*package.lock(&db), None);
2594 }
2595 }
2596
2597 const PACKAGE_A: &str = "[package]\nname = \"a\"\nversion = \"1.0.0\"\n";
2600 const PACKAGE_B: &str = "[package]\nname = \"b\"\nversion = \"1.0.0\"\n";
2601
2602 #[test]
2603 fn find_root_walks_from_a_member_to_its_workspace() {
2604 let dir = TempDir::new("find-root-member");
2605 dir.write("ridl.toml", "[workspace]\nmembers = [\"a\", \"b\"]\n");
2606 dir.write("a/ridl.toml", PACKAGE_A);
2607 dir.write("a/src/a.typl", "package a.src\n");
2608 dir.write("b/ridl.toml", PACKAGE_B);
2609
2610 assert_eq!(
2611 find_root(&dir.path().join("a/src")),
2612 Some(dir.path().to_path_buf())
2613 );
2614 assert_eq!(
2615 find_root(&dir.path().join("a")),
2616 Some(dir.path().to_path_buf())
2617 );
2618 }
2619
2620 #[test]
2622 fn find_root_normalises_the_member_path() {
2623 let dir = TempDir::new("find-root-normalise");
2624 dir.write("ridl.toml", "[workspace]\nmembers = [\"./a/\"]\n");
2625 dir.write("a/ridl.toml", PACKAGE_A);
2626
2627 assert_eq!(
2628 find_root(&dir.path().join("a")),
2629 Some(dir.path().to_path_buf())
2630 );
2631 }
2632
2633 #[test]
2634 fn find_root_keeps_an_unlisted_package_standalone() {
2635 let dir = TempDir::new("find-root-unlisted");
2636 dir.write("ridl.toml", "[workspace]\nmembers = [\"b\"]\n");
2637 dir.write("a/ridl.toml", PACKAGE_A);
2638 dir.write("b/ridl.toml", PACKAGE_B);
2639
2640 assert_eq!(find_root(&dir.path().join("a")), Some(dir.path().join("a")));
2641 }
2642
2643 #[test]
2644 fn find_root_stops_at_the_first_workspace() {
2645 let dir = TempDir::new("find-root-first-workspace");
2646 dir.write("ridl.toml", "[workspace]\nmembers = [\"inner/a\"]\n");
2647 dir.write("inner/ridl.toml", "[workspace]\nmembers = [\"b\"]\n");
2648 dir.write("inner/a/ridl.toml", PACKAGE_A);
2649
2650 assert_eq!(
2651 find_root(&dir.path().join("inner/a")),
2652 Some(dir.path().join("inner/a"))
2653 );
2654 }
2655
2656 #[test]
2657 fn find_root_stops_at_a_git_directory() {
2658 let dir = TempDir::new("find-root-git");
2659 dir.write("ridl.toml", "[workspace]\nmembers = [\"repo/a\"]\n");
2660 fs::create_dir_all(dir.path().join("repo/.git")).expect("create .git");
2661 dir.write("repo/a/ridl.toml", PACKAGE_A);
2662
2663 assert_eq!(
2664 find_root(&dir.path().join("repo/a")),
2665 Some(dir.path().join("repo/a"))
2666 );
2667 }
2668
2669 #[test]
2672 fn find_root_checks_the_manifest_beside_a_git_directory() {
2673 let dir = TempDir::new("find-root-git-root");
2674 dir.write("ridl.toml", "[workspace]\nmembers = [\"a\"]\n");
2675 fs::create_dir_all(dir.path().join(".git")).expect("create .git");
2676 dir.write("a/ridl.toml", PACKAGE_A);
2677
2678 assert_eq!(
2679 find_root(&dir.path().join("a")),
2680 Some(dir.path().to_path_buf())
2681 );
2682 }
2683
2684 #[test]
2687 fn find_root_returns_an_unreadable_manifest_above_the_package() {
2688 let dir = TempDir::new("find-root-unreadable");
2689 fs::write(dir.path().join("ridl.toml"), [0xff, 0xfe]).expect("write the manifest");
2690 dir.write("a/ridl.toml", PACKAGE_A);
2691
2692 assert_eq!(
2693 find_root(&dir.path().join("a")),
2694 Some(dir.path().to_path_buf())
2695 );
2696 }
2697
2698 #[test]
2702 fn up_keeps_the_relative_form_of_the_entry() {
2703 assert_eq!(up(Path::new("members/a"), 2), PathBuf::from("."));
2704 assert_eq!(up(Path::new("a"), 1), PathBuf::from("."));
2705 assert_eq!(up(Path::new("a"), 2), PathBuf::from(".."));
2706 assert_eq!(up(Path::new("."), 2), PathBuf::from("../.."));
2707 assert_eq!(up(Path::new(""), 1), PathBuf::from(".."));
2708 assert_eq!(up(Path::new("a/.."), 1), PathBuf::from("a/../.."));
2709 assert_eq!(up(Path::new("/w/a"), 1), PathBuf::from("/w"));
2710 }
2711
2712 #[test]
2713 fn member_entry_sets_report_scope() {
2714 let dir = TempDir::new("report-scope");
2715 dir.write("ridl.toml", "[workspace]\nmembers = [\"a\", \"b\"]\n");
2716 dir.write("a/ridl.toml", PACKAGE_A);
2717 dir.write("a/a.typl", "package a\ntype A: integer [0..1]\n");
2718 dir.write("b/ridl.toml", PACKAGE_B);
2719 let b_file = dir.write("b/b.typl", "package b\ntype B: integer [0..1]\n");
2720
2721 let mut db = RidlDatabase::default();
2722 let loaded = load_workspace(&mut db, &dir.path().join("a")).expect("the workspace loads");
2723 assert_eq!(loaded.report_scope, Some(dir.path().join("a")));
2724 let mut names: Vec<_> = loaded
2725 .workspace
2726 .packages(&db)
2727 .iter()
2728 .map(|p| p.name(&db).clone())
2729 .collect();
2730 names.sort();
2731 assert_eq!(names, vec!["a", "b"], "both members compile");
2732
2733 let mut db = RidlDatabase::default();
2734 let loaded = load_workspace(&mut db, &b_file).expect("the workspace loads");
2735 assert_eq!(loaded.report_scope, Some(dir.path().join("b")));
2736
2737 let mut db = RidlDatabase::default();
2738 let loaded = load_workspace(&mut db, dir.path()).expect("the workspace loads");
2739 assert_eq!(loaded.report_scope, None, "the root reports on everything");
2740 }
2741
2742 #[test]
2743 fn codegen_header_is_read_and_normalised() {
2744 let dir = TempDir::new("codegen-header");
2745 dir.write(
2746 "ridl.toml",
2747 &format!("{PACKAGE_A}\n[codegen]\nheader-file = \"H.txt\"\n"),
2748 );
2749 dir.write("a.typl", "package a\ntype A: integer [0..1]\n");
2750 dir.write("H.txt", "SPDX-License-Identifier: MIT\r\n");
2751 let mut db = RidlDatabase::default();
2752 let loaded = load_workspace(&mut db, dir.path()).expect("the package loads");
2753 assert!(loaded.diagnostics.is_empty(), "{:?}", loaded.diagnostics);
2754 assert_eq!(
2755 loaded.codegen_header.as_deref(),
2756 Some("SPDX-License-Identifier: MIT")
2757 );
2758 }
2759
2760 #[test]
2761 fn codegen_header_file_missing_is_mani_011() {
2762 let dir = TempDir::new("codegen-header-missing");
2763 let manifest = format!("{PACKAGE_A}\n[codegen]\nheader-file = \"nope.txt\"\n");
2764 dir.write("ridl.toml", &manifest);
2765 dir.write("a.typl", "package a\ntype A: integer [0..1]\n");
2766 let mut db = RidlDatabase::default();
2767 let loaded = load_workspace(&mut db, dir.path()).expect("the package loads");
2768 assert_eq!(codes(&loaded.diagnostics), vec!["MANI-011"]);
2769 let diag = &loaded.diagnostics[0];
2770 let start = usize::from(diag.primary.range.start());
2771 let end = usize::from(diag.primary.range.end());
2772 assert_eq!(&manifest[start..end], "\"nope.txt\"");
2773 let resolved = dir.path().join("nope.txt");
2774 assert!(
2775 diag.message.contains(&resolved.display().to_string()),
2776 "{}",
2777 diag.message
2778 );
2779 assert_eq!(loaded.codegen_header, None);
2780 }
2781
2782 #[test]
2783 fn codegen_header_file_that_is_not_utf8_is_mani_011() {
2784 let dir = TempDir::new("codegen-header-not-utf8");
2785 dir.write(
2786 "ridl.toml",
2787 &format!("{PACKAGE_A}\n[codegen]\nheader-file = \"H.txt\"\n"),
2788 );
2789 dir.write("a.typl", "package a\ntype A: integer [0..1]\n");
2790 fs::write(dir.path().join("H.txt"), [0xffu8, 0xfe]).expect("write the header");
2791 let mut db = RidlDatabase::default();
2792 let loaded = load_workspace(&mut db, dir.path()).expect("the package loads");
2793 assert_eq!(codes(&loaded.diagnostics), vec!["MANI-011"]);
2794 assert!(
2795 loaded.diagnostics[0].message.contains("H.txt"),
2796 "{}",
2797 loaded.diagnostics[0].message
2798 );
2799 assert_eq!(loaded.codegen_header, None);
2800 }
2801
2802 #[test]
2803 fn codegen_header_file_with_a_control_character_is_mani_011() {
2804 let dir = TempDir::new("codegen-header-control");
2805 dir.write(
2806 "ridl.toml",
2807 &format!("{PACKAGE_A}\n[codegen]\nheader-file = \"H.txt\"\n"),
2808 );
2809 dir.write("a.typl", "package a\ntype A: integer [0..1]\n");
2810 dir.write("H.txt", "A\u{2028}B\u{0}\n");
2811 let mut db = RidlDatabase::default();
2812 let loaded = load_workspace(&mut db, dir.path()).expect("the package loads");
2813 assert_eq!(codes(&loaded.diagnostics), vec!["MANI-011"]);
2814 let diag = &loaded.diagnostics[0];
2815 let manifest = fs::read_to_string(dir.path().join("ridl.toml")).unwrap();
2816 let start = usize::from(diag.primary.range.start());
2817 let end = usize::from(diag.primary.range.end());
2818 assert_eq!(&manifest[start..end], "\"H.txt\"");
2819 let message = &diag.message;
2820 assert!(message.contains("control character"), "{message}");
2821 assert!(message.contains("U+2028"), "{message}");
2822 assert!(
2823 message.contains(&dir.path().join("H.txt").display().to_string()),
2824 "{message}"
2825 );
2826 assert_eq!(loaded.codegen_header, None);
2827 }
2828
2829 #[test]
2830 fn codegen_header_file_in_a_member_is_mani_012() {
2831 let dir = TempDir::new("codegen-header-member");
2832 dir.write(
2833 "ridl.toml",
2834 "[workspace]\nmembers = [\"a\"]\n\n[codegen]\nheader-file = \"H.txt\"\n",
2835 );
2836 dir.write("H.txt", "root header\n");
2837 let member = format!("{PACKAGE_A}\n[codegen]\nheader-file = \"M.txt\"\n");
2838 dir.write("a/ridl.toml", &member);
2839 dir.write("a/a.typl", "package a\ntype A: integer [0..1]\n");
2840 let mut db = RidlDatabase::default();
2841 let loaded = load_workspace(&mut db, dir.path()).expect("the workspace loads");
2842 assert_eq!(codes(&loaded.diagnostics), vec!["MANI-012"]);
2843 let diag = &loaded.diagnostics[0];
2844 let start = usize::from(diag.primary.range.start());
2845 let end = usize::from(diag.primary.range.end());
2846 assert_eq!(&member[start..end], "\"M.txt\"");
2847 assert_eq!(loaded.codegen_header.as_deref(), Some("root header"));
2848 }
2849
2850 #[test]
2851 fn a_manifest_below_a_unit_is_mani_013_and_its_tree_is_not_loaded() {
2852 let dir = TempDir::new("nested-manifest");
2853 dir.write(
2854 "ridl.toml",
2855 "[package]\nname = \"veh.hmi\"\nversion = \"1.0.0\"\n",
2856 );
2857 dir.write("hmi.ridl", "package veh.hmi\n");
2858 let nested = dir.write(
2859 "cluster/ridl.toml",
2860 "[package]\nname = \"veh.hmi.cluster\"\nversion = \"1.0.0\"\n",
2861 );
2862 dir.write("cluster/x.ridl", "package veh.hmi.cluster\n");
2863 let mut db = RidlDatabase::default();
2864 let loaded = load_workspace(&mut db, dir.path()).expect("the unit loads");
2865 assert_eq!(codes(&loaded.diagnostics), vec!["MANI-013"]);
2866 let diag = &loaded.diagnostics[0];
2867 assert_eq!(
2868 loaded.sources.path(diag.primary.file),
2869 Some(path_string(&nested).as_str())
2870 );
2871 assert_eq!(diag.primary.range, byte_range(0, 0));
2872 assert!(diag.message.contains("unit `veh.hmi`"), "{}", diag.message);
2873 let packages = loaded.workspace.packages(&db);
2874 assert_eq!(packages.len(), 1);
2875 assert_eq!(packages[0].name(&db), "veh.hmi");
2876 }
2877
2878 #[test]
2879 fn a_member_whose_tree_holds_another_member_is_mani_013() {
2880 let dir = TempDir::new("nested-member");
2881 dir.write("ridl.toml", "[workspace]\nmembers = [\"a\", \"a/b\"]\n");
2882 dir.write(
2883 "a/ridl.toml",
2884 "[package]\nname = \"a\"\nversion = \"1.0.0\"\n",
2885 );
2886 dir.write("a/a.typl", "package a\n");
2887 dir.write(
2888 "a/b/ridl.toml",
2889 "[package]\nname = \"b\"\nversion = \"1.0.0\"\n",
2890 );
2891 dir.write("a/b/b.typl", "package b\n");
2892 let mut db = RidlDatabase::default();
2893 let loaded = load_workspace(&mut db, dir.path()).expect("the workspace loads");
2894 let count = codes(&loaded.diagnostics)
2895 .iter()
2896 .filter(|c| **c == "MANI-013")
2897 .count();
2898 assert_eq!(count, 1, "{:?}", codes(&loaded.diagnostics));
2899 }
2900
2901 #[test]
2902 fn a_root_package_already_claimed_by_a_sibling_tree_is_mani_014() {
2903 let dir = TempDir::new("claimed-twice");
2904 dir.write("ridl.toml", "[workspace]\nmembers = [\"base\", \"hmi\"]\n");
2905 dir.write(
2906 "base/ridl.toml",
2907 "[package]\nname = \"com.example\"\nversion = \"1.0.0\"\n",
2908 );
2909 dir.write("base/hmi/x.ridl", "package com.example.hmi\n");
2910 let second = "[package]\nname = \"com.example.hmi\"\nversion = \"1.0.0\"\n";
2911 let second_path = dir.write("hmi/ridl.toml", second);
2912 dir.write("hmi/y.ridl", "package com.example.hmi\n");
2913 let mut db = RidlDatabase::default();
2914 let loaded = load_workspace(&mut db, dir.path()).expect("the workspace loads");
2915 assert_eq!(codes(&loaded.diagnostics), vec!["MANI-014"]);
2916 let diag = &loaded.diagnostics[0];
2917 assert_eq!(
2918 loaded.sources.path(diag.primary.file),
2919 Some(path_string(&second_path).as_str())
2920 );
2921 let start = usize::from(diag.primary.range.start());
2922 let end = usize::from(diag.primary.range.end());
2923 assert_eq!(&second[start..end], "\"com.example.hmi\"");
2924 assert!(
2925 diag.message.contains("unit `com.example`")
2926 && diag.message.contains("unit `com.example.hmi`"),
2927 "{}",
2928 diag.message
2929 );
2930 let packages = loaded.workspace.packages(&db);
2931 let claimed: Vec<_> = packages
2932 .iter()
2933 .filter(|p| p.name(&db) == "com.example.hmi")
2934 .collect();
2935 assert_eq!(claimed.len(), 1);
2936 assert_eq!(claimed[0].unit(&db), "com.example");
2937 }
2938
2939 fn two_members_with_one_name(members: &str, first: &str, second: &str) {
2944 let dir = TempDir::new("one-name-twice");
2945 dir.write("ridl.toml", &format!("[workspace]\nmembers = {members}\n"));
2946 let manifest = "[package]\nname = \"x\"\nversion = \"1.0.0\"\n";
2947 dir.write(&format!("{first}/ridl.toml"), manifest);
2948 dir.write(
2949 &format!("{first}/x.ridl"),
2950 &format!("package x\n\ninterface {}A {{}}\n", first.to_uppercase()),
2951 );
2952 let second_path = dir.write(&format!("{second}/ridl.toml"), manifest);
2953 dir.write(
2954 &format!("{second}/x.ridl"),
2955 &format!("package x\n\ninterface {}A {{}}\n", second.to_uppercase()),
2956 );
2957 let mut db = RidlDatabase::default();
2958 let loaded = load_workspace(&mut db, dir.path()).expect("the workspace loads");
2959 assert_eq!(codes(&loaded.diagnostics), vec!["MANI-014"]);
2960 let diag = &loaded.diagnostics[0];
2961 assert_eq!(
2962 loaded.sources.path(diag.primary.file),
2963 Some(path_string(&second_path).as_str())
2964 );
2965 let start = usize::from(diag.primary.range.start());
2966 let end = usize::from(diag.primary.range.end());
2967 assert_eq!(&manifest[start..end], "\"x\"");
2968 assert!(
2969 diag.message.contains("source package `x`"),
2970 "{}",
2971 diag.message
2972 );
2973 assert_eq!(loaded.units.get("x"), Some(&dir.path().join(first)));
2974 let packages = loaded.workspace.packages(&db);
2975 assert_eq!(packages.len(), 1);
2976 let files = packages[0].files(&db);
2977 assert_eq!(files.len(), 1);
2978 assert!(
2979 files[0]
2980 .text(&db)
2981 .contains(&format!("{}A", first.to_uppercase())),
2982 "the package holds `{first}`'s file"
2983 );
2984 }
2985
2986 #[test]
2987 fn two_members_with_one_name_are_mani_014_on_the_second() {
2988 two_members_with_one_name("[\"a\", \"b\"]", "a", "b");
2989 }
2990
2991 #[test]
2992 fn two_members_with_one_name_are_mani_014_on_the_second_in_reverse_order() {
2993 two_members_with_one_name("[\"b\", \"a\"]", "b", "a");
2994 }
2995
2996 #[test]
2997 fn two_units_with_a_shared_prefix_and_no_overlap_both_load() {
2998 let dir = TempDir::new("shared-prefix");
2999 dir.write("ridl.toml", "[workspace]\nmembers = [\"base\", \"hmi\"]\n");
3000 dir.write(
3001 "base/ridl.toml",
3002 "[package]\nname = \"com.example\"\nversion = \"1.0.0\"\n",
3003 );
3004 dir.write("base/ids.typl", "package com.example\n");
3005 dir.write(
3006 "hmi/ridl.toml",
3007 "[package]\nname = \"com.example.hmi\"\nversion = \"1.0.0\"\n",
3008 );
3009 dir.write("hmi/y.ridl", "package com.example.hmi\n");
3010 let mut db = RidlDatabase::default();
3011 let loaded = load_workspace(&mut db, dir.path()).expect("the workspace loads");
3012 assert!(
3013 loaded.diagnostics.is_empty(),
3014 "{:?}",
3015 codes(&loaded.diagnostics)
3016 );
3017 let units: std::collections::BTreeSet<String> = loaded
3018 .workspace
3019 .packages(&db)
3020 .iter()
3021 .map(|p| p.unit(&db).to_string())
3022 .collect();
3023 assert_eq!(
3024 units,
3025 ["com.example", "com.example.hmi"].map(String::from).into()
3026 );
3027 }
3028}