1use std::collections::BTreeMap;
35use std::fs;
36use std::io;
37use std::path::{Component, Path, PathBuf};
38
39use ridl_ir::codegen::{header_control_character, normalise_header};
40use ridl_syntax::ast::{AstNode as _, SourceFile};
41use rowan::{TextRange, TextSize};
42
43use crate::db::{InputFile, RidlDatabase, parse_file};
44use crate::diag::{DiagCode, Diagnostic, FileId, Severity, SourceMap, Span};
45use crate::interface_lock;
46use crate::lint::{LintLevels, LintScopes};
47use crate::manifest::{Manifest, ManifestKind, TimingDefaults, parse_manifest};
48use crate::package::{Package, PackageLock, PackageOrigin, Workspace, package_declarations};
49
50pub struct LoadedWorkspace {
59 pub workspace: Workspace,
60 pub diagnostics: Vec<Diagnostic>,
61 pub sources: SourceMap,
62 pub lints: LintScopes,
63 pub codegen_header: Option<String>,
68 pub report_scope: Option<PathBuf>,
77 pub units: BTreeMap<String, PathBuf>,
82}
83
84#[derive(Clone, Debug, PartialEq, Eq)]
86pub struct Overlay {
87 pub path: PathBuf,
88 pub text: String,
89}
90
91#[derive(Debug)]
97pub enum LoadError {
98 Io(io::Error),
99 OverlayNotSource(PathBuf),
100 OverlayOutsideWorkspace {
101 path: PathBuf,
102 missing_directory: bool,
103 },
104}
105
106impl std::fmt::Display for LoadError {
107 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108 match self {
109 Self::Io(e) => write!(f, "{e}"),
110 Self::OverlayNotSource(p) => write!(
111 f,
112 "overlay `{}` is not a `.typl`, `.ridl`, `.rsdl`, `.rxdl` or `.rmdl` file",
113 p.display()
114 ),
115 Self::OverlayOutsideWorkspace {
116 path,
117 missing_directory: true,
118 } => write!(
119 f,
120 "overlay `{}` is in a directory that does not exist; create the directory first",
121 path.display()
122 ),
123 Self::OverlayOutsideWorkspace {
124 path,
125 missing_directory: false,
126 } => write!(
127 f,
128 "overlay `{}` is not in a package directory of the workspace loaded from this path",
129 path.display()
130 ),
131 }
132 }
133}
134
135impl std::error::Error for LoadError {
136 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
137 match self {
138 Self::Io(e) => Some(e),
139 _ => None,
140 }
141 }
142}
143
144impl From<io::Error> for LoadError {
145 fn from(e: io::Error) -> Self {
146 Self::Io(e)
147 }
148}
149
150fn unsupported_extension(path: &Path) -> Option<&str> {
154 path.extension()
155 .and_then(|ext| ext.to_str())
156 .filter(|ext| *ext == "rxdl" || *ext == "rmdl")
157}
158
159fn overlay_key(path: &Path) -> Option<PathBuf> {
162 let absolute = if path.is_absolute() {
163 path.to_path_buf()
164 } else {
165 std::env::current_dir().ok()?.join(path)
166 };
167 Some(
168 absolute
169 .parent()?
170 .canonicalize()
171 .ok()?
172 .join(absolute.file_name()?),
173 )
174}
175
176pub fn load_workspace(db: &mut RidlDatabase, entry: &Path) -> io::Result<LoadedWorkspace> {
203 load_workspace_with(db, entry, &[]).map_err(|error| match error {
204 LoadError::Io(e) => e,
205 _ => unreachable!("no overlay error is possible without overlays"),
206 })
207}
208
209pub fn load_workspace_with(
211 db: &mut RidlDatabase,
212 entry: &Path,
213 overlays: &[Overlay],
214) -> Result<LoadedWorkspace, LoadError> {
215 let mut loader = Loader::default();
216 for overlay in overlays {
217 if unsupported_extension(&overlay.path).is_none()
220 && !overlay
221 .path
222 .extension()
223 .is_some_and(|ext| ext == "typl" || ext == "ridl" || ext == "rsdl")
224 {
225 return Err(LoadError::OverlayNotSource(overlay.path.clone()));
226 }
227 let key = overlay_key(&overlay.path).ok_or_else(|| LoadError::OverlayOutsideWorkspace {
228 path: overlay.path.clone(),
229 missing_directory: true,
230 })?;
231 loader.overlays.push((key, overlay.clone(), false));
232 }
233
234 if entry.is_file() {
235 match entry.parent().and_then(find_root) {
236 Some(root) => loader.load_root(db, &root)?,
237 None => loader.load_single_file(db, entry)?,
238 }
239 } else if entry.is_dir() {
240 match find_root(entry) {
241 Some(root) => loader.load_root(db, &root)?,
242 None => {
243 if let Some(error) = unreadable_manifest(entry) {
244 return Err(error.into());
245 }
246 return Err(io::Error::new(
247 io::ErrorKind::NotFound,
248 format!("no `ridl.toml` found at or above `{}`", entry.display()),
249 )
250 .into());
251 }
252 }
253 } else {
254 return Err(io::Error::new(
255 io::ErrorKind::NotFound,
256 format!("`{}` does not exist", entry.display()),
257 )
258 .into());
259 }
260
261 if let Some((_, overlay, _)) = loader.overlays.iter().find(|(_, _, consumed)| !consumed) {
262 return Err(LoadError::OverlayOutsideWorkspace {
263 path: overlay.path.clone(),
264 missing_directory: false,
265 });
266 }
267 let report_scope = absolute(entry).and_then(|entry| {
268 loader
269 .member_dirs
270 .iter()
271 .find(|member| absolute(member).is_some_and(|member| entry.starts_with(member)))
272 .cloned()
273 });
274 let workspace = Workspace::new(&*db, loader.packages, loader.workspace_imports);
275 Ok(LoadedWorkspace {
276 workspace,
277 diagnostics: loader.diagnostics,
278 sources: loader.sources,
279 lints: loader.lints,
280 codegen_header: loader.codegen_header,
281 report_scope,
282 units: loader.units,
283 })
284}
285
286pub fn find_root(dir: &Path) -> Option<PathBuf> {
306 let package = dir
307 .ancestors()
308 .find(|candidate| candidate.join("ridl.toml").is_file())?
309 .to_path_buf();
310 if !matches!(
311 read_manifest_kind(&package),
312 Some(ManifestKind::Package { .. })
313 ) {
314 return Some(package);
315 }
316 let Some(absolute_package) = absolute(&package) else {
317 return Some(package);
318 };
319 for (levels, parent) in absolute_package.ancestors().skip(1).enumerate() {
320 if parent.join("ridl.toml").is_file() {
321 match read_manifest_kind(parent) {
322 None => return Some(up(&package, levels + 1)),
323 Some(ManifestKind::Workspace { members }) => {
324 let listed = members
325 .iter()
326 .any(|member| normalize(&parent.join(member)) == absolute_package);
327 return Some(if listed {
328 up(&package, levels + 1)
329 } else {
330 package
331 });
332 }
333 Some(ManifestKind::Package { .. }) => {}
334 }
335 }
336 if parent.join(".git").exists() {
337 break;
338 }
339 }
340 Some(package)
341}
342
343fn unreadable_manifest(dir: &Path) -> Option<io::Error> {
351 dir.ancestors().find_map(|candidate| {
352 let manifest = candidate.join("ridl.toml");
353 match fs::metadata(&manifest) {
354 Err(e) if e.kind() != io::ErrorKind::NotFound => Some(io::Error::new(
355 e.kind(),
356 format!("cannot read `{}`: {e}", manifest.display()),
357 )),
358 _ => None,
359 }
360 })
361}
362
363fn cannot_read(path: &Path, error: io::Error) -> io::Error {
365 io::Error::new(
366 error.kind(),
367 format!("cannot read `{}`: {error}", path.display()),
368 )
369}
370
371fn read_manifest_kind(dir: &Path) -> Option<ManifestKind> {
374 let text = fs::read_to_string(dir.join("ridl.toml")).ok()?;
375 parse_manifest(FileId::DETACHED, &text)
376 .0
377 .map(|manifest| manifest.kind)
378}
379
380fn absolute(path: &Path) -> Option<PathBuf> {
383 if path.is_absolute() {
384 Some(normalize(path))
385 } else {
386 Some(normalize(&std::env::current_dir().ok()?.join(path)))
387 }
388}
389
390fn normalize(path: &Path) -> PathBuf {
393 let mut normalized = PathBuf::new();
394 for component in path.components() {
395 match component {
396 Component::CurDir => {}
397 Component::ParentDir => {
398 if matches!(
399 normalized.components().next_back(),
400 Some(Component::Normal(_))
401 ) {
402 normalized.pop();
403 } else if !normalized.has_root() {
404 normalized.push("..");
405 }
406 }
407 other => normalized.push(other),
408 }
409 }
410 normalized
411}
412
413fn up(path: &Path, levels: usize) -> PathBuf {
420 let mut result = path.to_path_buf();
421 for _ in 0..levels {
422 match result.components().next_back() {
423 Some(Component::Normal(_)) => {
424 result.pop();
425 if result.as_os_str().is_empty() {
426 result = PathBuf::from(".");
427 }
428 }
429 Some(Component::RootDir | Component::Prefix(_)) => {}
430 Some(Component::CurDir) | None => result = PathBuf::from(".."),
431 Some(Component::ParentDir) => result.push(".."),
432 }
433 }
434 result
435}
436
437type LoadedFile = (InputFile, Vec<(String, TextRange)>);
440
441#[derive(Clone, Copy)]
443struct TreeScope<'a> {
444 unit_dir: &'a Path,
446 unit: &'a str,
448 imports: &'a BTreeMap<String, String>,
450 defaults: &'a TimingDefaults,
452 lock: &'a Option<PackageLock>,
454}
455
456#[derive(Default)]
458struct Loader {
459 claims: BTreeMap<String, (String, PathBuf)>,
462 overlays: Vec<(PathBuf, Overlay, bool)>,
463 sources: SourceMap,
464 diagnostics: Vec<Diagnostic>,
465 packages: Vec<Package>,
466 workspace_imports: BTreeMap<String, String>,
469 workspace_defaults: TimingDefaults,
474 workspace_lints: LintLevels,
479 lints: LintScopes,
484 codegen_header: Option<String>,
486 member_dirs: Vec<PathBuf>,
489 units: BTreeMap<String, PathBuf>,
491 unit_name_spans: BTreeMap<String, (FileId, TextRange)>,
494}
495
496impl Loader {
497 fn load_root(&mut self, db: &mut RidlDatabase, root: &Path) -> io::Result<()> {
500 let manifest_path = root.join("ridl.toml");
501 let text = fs::read_to_string(&manifest_path).map_err(|e| {
504 io::Error::new(
505 e.kind(),
506 format!("cannot read `{}`: {e}", manifest_path.display()),
507 )
508 })?;
509 let file_id = self.sources.file_id(&path_string(&manifest_path), &text);
510 let (manifest, diags) = parse_manifest(file_id, &text);
511 self.diagnostics.extend(diags);
512 let Some(Manifest {
513 kind,
514 imports,
515 defaults,
516 lints,
517 codegen_header_file,
518 }) = manifest
519 else {
520 return Ok(());
521 };
522 if let Some((relative, range)) = codegen_header_file {
523 let path = root.join(&relative);
524 match fs::read_to_string(&path) {
525 Ok(header) => match header_control_character(&header) {
526 None => self.codegen_header = normalise_header(&header),
527 Some(c) => self.diagnostics.push(error(
528 DiagCode::MANI_011,
529 file_id,
530 byte_range(range.start, range.end),
531 format!(
532 "`[codegen] header-file` contains a control character \
533 (U+{:04X}): `{}`",
534 u32::from(c),
535 path.display()
536 ),
537 )),
538 },
539 Err(e) => self.diagnostics.push(error(
540 DiagCode::MANI_011,
541 file_id,
542 byte_range(range.start, range.end),
543 format!(
544 "`[codegen] header-file` cannot be read: `{}`: {e}",
545 path.display()
546 ),
547 )),
548 }
549 }
550 let mut root_lints = LintLevels::default();
554 root_lints.overlay(&lints);
555 self.lints.insert(root.to_path_buf(), root_lints.clone());
556 match kind {
557 ManifestKind::Package {
558 name, name_span, ..
559 } => {
560 self.units.insert(name.clone(), root.to_path_buf());
564 self.unit_name_spans.insert(
565 name.clone(),
566 (file_id, byte_range(name_span.start, name_span.end)),
567 );
568 let lock = self.read_lock(root)?;
569 let scope = TreeScope {
570 unit_dir: root,
571 unit: &name,
572 imports: &imports,
573 defaults: &defaults,
574 lock: &lock,
575 };
576 self.load_package_tree(db, root, &name, &scope)?;
577 }
578 ManifestKind::Workspace { members } => {
579 self.workspace_imports = imports;
583 self.workspace_defaults = defaults;
584 self.workspace_lints = root_lints;
585 for member in &members {
586 let member_dir = root.join(member);
592 let normalized = normalize(&member_dir);
593 if self
594 .member_dirs
595 .iter()
596 .any(|dir| normalize(dir) == normalized)
597 {
598 continue;
599 }
600 self.member_dirs.push(member_dir);
601 self.load_member(db, root, member, file_id, &text)?;
602 }
603 }
604 }
605 Ok(())
606 }
607
608 fn load_member(
612 &mut self,
613 db: &mut RidlDatabase,
614 workspace_root: &Path,
615 member: &str,
616 workspace_file: FileId,
617 workspace_text: &str,
618 ) -> io::Result<()> {
619 let manifest_path = workspace_root.join(member).join("ridl.toml");
620 if !manifest_path.is_file() {
621 self.diagnostics.push(error(
624 DiagCode::MANI_008,
625 workspace_file,
626 member_entry_range(workspace_text, member),
627 format!("workspace member `{member}` has no `ridl.toml`"),
628 ));
629 return Ok(());
630 }
631 let text =
632 fs::read_to_string(&manifest_path).map_err(|e| cannot_read(&manifest_path, e))?;
633 let file_id = self.sources.file_id(&path_string(&manifest_path), &text);
634 let (manifest, diags) = parse_manifest(file_id, &text);
635 self.diagnostics.extend(diags);
636 let Some(Manifest {
637 kind,
638 imports,
639 defaults,
640 lints,
641 codegen_header_file,
642 }) = manifest
643 else {
644 return Ok(());
645 };
646 if let Some((_, range)) = codegen_header_file {
647 self.diagnostics.push(error(
648 DiagCode::MANI_012,
649 file_id,
650 byte_range(range.start, range.end),
651 format!(
652 "`[codegen] header-file` is set in workspace member `{member}`; set it in the workspace root's `ridl.toml`"
653 ),
654 ));
655 }
656 let mut member_lints = self.workspace_lints.clone();
661 member_lints.overlay(&lints);
662 self.lints.insert(workspace_root.join(member), member_lints);
663 match kind {
664 ManifestKind::Workspace { .. } => {
665 self.diagnostics.push(error(
666 DiagCode::MANI_004,
667 file_id,
668 workspace_section_range(&text),
669 format!(
670 "workspace member `{member}` declares `[workspace]`; nested workspaces are forbidden"
671 ),
672 ));
673 }
674 ManifestKind::Package {
675 name, name_span, ..
676 } => {
677 let member_defaults = defaults.or(&self.workspace_defaults);
683 let member_dir = workspace_root.join(member);
684 let name_range = byte_range(name_span.start, name_span.end);
692 if let Some(first_dir) = self.units.get(&name) {
693 let first_dir = first_dir.clone();
694 self.diagnostics.push(error(
695 DiagCode::MANI_014,
696 file_id,
697 name_range,
698 format!(
699 "source package `{name}` is already declared by the unit in `{}`; the unit in `{}` declares it too. A source package belongs to one unit",
700 first_dir.display(),
701 member_dir.display()
702 ),
703 ));
704 return Ok(());
705 }
706 self.units.insert(name.clone(), member_dir.clone());
707 self.unit_name_spans
708 .insert(name.clone(), (file_id, name_range));
709 let lock = self.read_lock(&member_dir)?;
710 let scope = TreeScope {
711 unit_dir: &member_dir,
712 unit: &name,
713 imports: &imports,
714 defaults: &member_defaults,
715 lock: &lock,
716 };
717 self.load_package_tree(db, &member_dir, &name, &scope)?;
718 }
719 }
720 Ok(())
721 }
722
723 fn load_package_tree(
745 &mut self,
746 db: &mut RidlDatabase,
747 dir: &Path,
748 name: &str,
749 scope: &TreeScope<'_>,
750 ) -> io::Result<()> {
751 let TreeScope {
752 unit_dir,
753 unit,
754 imports,
755 defaults,
756 lock,
757 } = *scope;
758 let mut source_files = Vec::new();
759 let mut unsupported_files = Vec::new();
760 let mut subdirs = Vec::new();
761 let named = |e: io::Error| {
762 io::Error::new(e.kind(), format!("cannot read `{}`: {e}", dir.display()))
763 };
764 for entry in fs::read_dir(dir).map_err(named)? {
765 let entry = entry.map_err(named)?;
766 let path = entry.path();
767 let is_symlink = entry.file_type().map_err(named)?.is_symlink();
768 if path.is_dir() {
769 if !is_symlink {
770 subdirs.push(path);
771 }
772 } else if path
773 .extension()
774 .is_some_and(|ext| ext == "typl" || ext == "ridl" || ext == "rsdl")
775 {
776 source_files.push(path);
777 } else if unsupported_extension(&path).is_some() {
778 unsupported_files.push(path);
779 }
780 }
781 if !self.overlays.is_empty() {
782 let directory_key = dir.canonicalize()?;
783 for (key, _, _) in &self.overlays {
784 let files = if unsupported_extension(key).is_some() {
785 &mut unsupported_files
786 } else {
787 &mut source_files
788 };
789 if key.parent() == Some(directory_key.as_path())
790 && !files.iter().any(|p| overlay_key(p).as_ref() == Some(key))
791 {
792 let added = dir.join(key.file_name().expect("overlay keys have a file name"));
793 if !files.contains(&added) {
794 files.push(added);
795 }
796 }
797 }
798 }
799 source_files.sort();
800 unsupported_files.sort();
801 for path in &unsupported_files {
802 self.report_unsupported_file(path);
803 }
804 subdirs.sort();
805
806 let mut claimed_elsewhere = false;
807 if !source_files.is_empty() {
808 match self.claims.get(name) {
809 Some((first, first_dir)) if normalize(first_dir) != normalize(unit_dir) => {
813 claimed_elsewhere = true;
814 let (file, name_range) = self.unit_name_spans[unit];
817 self.diagnostics.push(error(
818 DiagCode::MANI_014,
819 file,
820 name_range,
821 format!(
822 "source package `{name}` is already declared by unit `{first}` (`{}`); unit `{unit}` declares it too, in `{}`. A source package belongs to one unit",
823 first_dir.display(),
824 unit_dir.display()
825 ),
826 ));
827 }
828 _ => {
829 self.claims
830 .insert(name.to_string(), (unit.to_string(), unit_dir.to_path_buf()));
831 }
832 }
833 }
834 if !source_files.is_empty() && !claimed_elsewhere {
835 let mut files = Vec::new();
836 for path in &source_files {
837 if let Some((input, _)) = self.load_file(db, path, Some(name))? {
838 files.push(input);
839 }
840 }
841 self.packages.push(Package::new(
842 &*db,
843 name.to_string(),
844 unit.to_string(),
845 files,
846 PackageOrigin::WorkspaceMember,
847 imports.clone(),
848 defaults.clone(),
849 lock.clone(),
850 ));
851 }
852
853 for subdir in subdirs {
854 let Some(dir_name) = subdir.file_name().map(|n| n.to_string_lossy().into_owned())
855 else {
856 continue;
857 };
858 if dir_name.starts_with('.') {
859 continue;
860 }
861 let nested_manifest = subdir.join("ridl.toml");
862 if nested_manifest.is_file() {
863 let nested_text = fs::read_to_string(&nested_manifest)
864 .map_err(|e| cannot_read(&nested_manifest, e))?;
865 let nested_id = self
866 .sources
867 .file_id(&path_string(&nested_manifest), &nested_text);
868 self.diagnostics.push(error(
869 DiagCode::MANI_013,
870 nested_id,
871 byte_range(0, 0),
872 format!(
873 "`{}` 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",
874 nested_manifest.display()
875 ),
876 ));
877 continue;
878 }
879 let ignored = subdir.join(interface_lock::FILE_NAME);
880 if ignored.is_file() {
881 let ignored_text = fs::read_to_string(&ignored).unwrap_or_default();
882 let ignored_id = self.sources.file_id(&path_string(&ignored), &ignored_text);
883 self.diagnostics.push(warning(
884 DiagCode::RIDL_416,
885 ignored_id,
886 byte_range(0, 0),
887 format!(
888 "`{}` 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",
889 ignored.display()
890 ),
891 ));
892 }
893 self.load_package_tree(db, &subdir, &format!("{name}.{dir_name}"), scope)?;
894 }
895 Ok(())
896 }
897
898 fn load_single_file(&mut self, db: &mut RidlDatabase, path: &Path) -> io::Result<()> {
909 if let Some(extension) = unsupported_extension(path) {
910 return Err(io::Error::new(
911 io::ErrorKind::InvalidInput,
912 format!(
913 "`{}` is not compiled: `.{extension}` is not a supported source file extension (the {extension} profile has no implementation)",
914 path.display()
915 ),
916 ));
917 }
918 let Some((input, decls)) = self.load_file(db, path, None)? else {
919 return Ok(());
921 };
922 let name = decls
923 .first()
924 .map(|(name, _)| name.clone())
925 .filter(|name| !name.is_empty())
926 .unwrap_or_else(|| {
927 path.file_stem()
928 .map(|stem| stem.to_string_lossy().into_owned())
929 .unwrap_or_else(|| "package".to_string())
930 });
931 let lock = match path.parent() {
934 Some(dir) => self.read_lock(dir)?,
935 None => None,
936 };
937 self.packages.push(Package::new(
938 &*db,
939 name.clone(),
940 name.clone(),
941 vec![input],
942 PackageOrigin::WorkspaceMember,
943 BTreeMap::new(),
944 TimingDefaults::default(),
945 lock,
946 ));
947 if let Some(dir) = path.parent() {
948 self.units.insert(name, dir.to_path_buf());
949 }
950 Ok(())
951 }
952
953 fn report_unsupported_file(&mut self, path: &Path) {
959 let text = self.take_overlay(path).unwrap_or_default();
960 let file_id = self.sources.file_id(&path_string(path), &text);
961 let extension = unsupported_extension(path).expect("only unsupported files are reported");
962 self.diagnostics.push(warning(
963 DiagCode::RIDL_417,
964 file_id,
965 byte_range(0, 0),
966 format!(
967 "`{}` is a `.{extension}` file; the {extension} profile is not supported yet, so this file is not compiled",
968 path.display()
969 ),
970 ));
971 }
972
973 fn read_lock(&mut self, dir: &Path) -> io::Result<Option<PackageLock>> {
981 let path = path_string(&dir.join(interface_lock::FILE_NAME));
982 let text = match interface_lock::read(dir) {
983 Ok(Some(text)) => text,
984 Ok(None) => return Ok(None),
985 Err(err) if err.kind() == io::ErrorKind::InvalidData => {
986 let file_id = self.sources.file_id(&path, "");
987 self.diagnostics.push(error(
988 DiagCode::RIDL_410,
989 file_id,
990 byte_range(0, 0),
991 malformed_lock_message("the file is not valid UTF-8"),
992 ));
993 return Ok(None);
994 }
995 Err(err) => return Err(err),
996 };
997 match interface_lock::parse(&text) {
998 Ok(lock) => Ok(Some(PackageLock { path, text, lock })),
999 Err(malformed) => {
1000 let file_id = self.sources.file_id(&path, &text);
1001 self.diagnostics.push(error(
1002 DiagCode::RIDL_410,
1003 file_id,
1004 malformed.range,
1005 malformed_lock_message(&malformed.message),
1006 ));
1007 Ok(None)
1008 }
1009 }
1010 }
1011
1012 fn take_overlay(&mut self, path: &Path) -> Option<String> {
1015 let path_key = overlay_key(path)?;
1016 self.overlays
1017 .iter_mut()
1018 .filter(|(key, _, _)| *key == path_key)
1019 .map(|(_, overlay, consumed)| {
1020 *consumed = true;
1021 overlay.text.clone()
1022 })
1023 .last()
1024 }
1025
1026 fn load_file(
1034 &mut self,
1035 db: &mut RidlDatabase,
1036 path: &Path,
1037 expected: Option<&str>,
1038 ) -> io::Result<Option<LoadedFile>> {
1039 let path_str = path_string(path);
1040 let replacement = self.take_overlay(path);
1041 let text = match replacement
1042 .map(Ok)
1043 .unwrap_or_else(|| fs::read_to_string(path).map_err(|e| cannot_read(path, e)))
1044 {
1045 Ok(text) => text,
1046 Err(err) if err.kind() == io::ErrorKind::InvalidData => {
1047 let file_id = self.sources.file_id(&path_str, "");
1051 self.diagnostics.push(error(
1052 DiagCode::NONE,
1053 file_id,
1054 byte_range(0, 0),
1055 format!("`{path_str}` is not valid UTF-8; the file is skipped"),
1056 ));
1057 return Ok(None);
1058 }
1059 Err(err) => return Err(err),
1060 };
1061 let file_id = self.sources.file_id(&path_str, &text);
1062 let input = InputFile::new(&*db, path_str, text);
1063
1064 let parse = parse_file(&*db, input);
1065 let source =
1066 SourceFile::cast(parse.syntax()).expect("parser roots every tree in a SourceFile");
1067 let decls = package_declarations(&source);
1068
1069 for (_, range) in decls.iter().skip(1) {
1070 self.diagnostics.push(error(
1071 DiagCode::TYPL_001,
1072 file_id,
1073 *range,
1074 "more than one `package` declaration in this file".to_string(),
1075 ));
1076 }
1077 if let Some((declared, range)) = decls.first()
1078 && crate::std_lib::is_reserved_package_name(declared)
1079 {
1080 self.diagnostics.push(error(
1094 DiagCode::TYPL_010,
1095 file_id,
1096 *range,
1097 format!(
1098 "`{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"
1099 ),
1100 ));
1101 }
1102 if let (Some(expected), Some((declared, range))) = (expected, decls.first())
1103 && !declared.is_empty()
1104 && declared != expected
1105 {
1106 self.diagnostics.push(error(
1107 DiagCode::TYPL_002,
1108 file_id,
1109 *range,
1110 format!(
1111 "package name `{declared}` does not mirror the directory path; every file in this directory must declare `package {expected}`"
1112 ),
1113 ));
1114 }
1115 Ok(Some((input, decls)))
1116 }
1117}
1118
1119fn path_string(path: &Path) -> String {
1121 path.to_string_lossy().into_owned()
1122}
1123
1124fn malformed_lock_message(reason: &str) -> String {
1127 format!(
1128 "`{}` is malformed: {reason} — resolve the conflict or restore the file from version \
1129 control, then run `ridl lock`",
1130 interface_lock::FILE_NAME
1131 )
1132}
1133
1134fn member_entry_range(text: &str, member: &str) -> TextRange {
1138 let quoted = format!("\"{member}\"");
1139 match text.find("ed) {
1140 Some(start) => byte_range(start, start + quoted.len()),
1141 None => byte_range(0, text.len()),
1142 }
1143}
1144
1145fn workspace_section_range(text: &str) -> TextRange {
1148 const HEADER: &str = "[workspace]";
1149 match text.find(HEADER) {
1150 Some(start) => byte_range(start, start + HEADER.len()),
1151 None => byte_range(0, text.len()),
1152 }
1153}
1154
1155fn byte_range(start: usize, end: usize) -> TextRange {
1157 TextRange::new(TextSize::from(start as u32), TextSize::from(end as u32))
1158}
1159
1160fn error(code: DiagCode, file: FileId, range: TextRange, message: String) -> Diagnostic {
1163 Diagnostic {
1164 code,
1165 severity: Severity::Error,
1166 message,
1167 primary: Span { file, range },
1168 labels: Vec::new(),
1169 fixits: Vec::new(),
1170 }
1171}
1172
1173fn warning(code: DiagCode, file: FileId, range: TextRange, message: String) -> Diagnostic {
1174 Diagnostic {
1175 severity: Severity::Warning,
1176 ..error(code, file, range, message)
1177 }
1178}
1179
1180#[cfg(test)]
1181mod tests {
1182 use std::path::PathBuf;
1183 use std::sync::atomic::{AtomicUsize, Ordering};
1184
1185 use salsa::Setter;
1186 use salsa::plumbing::AsId;
1187
1188 use super::*;
1189 use crate::lint::{LintLevel, apply_lint_levels, lint_by_name};
1190
1191 struct TempDir(PathBuf);
1193
1194 impl TempDir {
1195 fn new(label: &str) -> Self {
1196 static COUNTER: AtomicUsize = AtomicUsize::new(0);
1197 let mut path = std::env::temp_dir();
1198 path.push(format!(
1199 "ridl-core-workspace-{label}-{}-{}",
1200 std::process::id(),
1201 COUNTER.fetch_add(1, Ordering::SeqCst),
1202 ));
1203 fs::create_dir_all(&path).expect("create the temp dir");
1204 Self(path)
1205 }
1206
1207 fn path(&self) -> &Path {
1208 &self.0
1209 }
1210
1211 fn write(&self, relative: &str, text: &str) -> PathBuf {
1213 let path = self.0.join(relative);
1214 fs::create_dir_all(path.parent().expect("relative paths have a parent"))
1215 .expect("create parent directories");
1216 fs::write(&path, text).expect("write the fixture file");
1217 path
1218 }
1219 }
1220
1221 impl Drop for TempDir {
1222 fn drop(&mut self) {
1223 let _ = fs::remove_dir_all(&self.0);
1224 }
1225 }
1226
1227 fn codes(diags: &[Diagnostic]) -> Vec<&str> {
1228 diags.iter().map(|d| d.code.as_str()).collect()
1229 }
1230
1231 fn overlay_fixture() -> (TempDir, PathBuf) {
1232 let dir = TempDir::new("overlay");
1233 dir.write(
1234 "p/ridl.toml",
1235 "[package]\nname = \"p\"\nversion = \"1.0.0\"\n",
1236 );
1237 let path = dir.write("p/a.typl", "package p\ntype A: integer [0..1]\n");
1238 (dir, path)
1239 }
1240
1241 fn overlay(path: PathBuf, text: &str) -> Overlay {
1242 Overlay {
1243 path,
1244 text: text.to_string(),
1245 }
1246 }
1247
1248 #[test]
1249 fn overlay_replaces_the_text_of_a_file_on_disk() {
1250 let (dir, path) = overlay_fixture();
1251 let text = "package p\n\ntype Other: integer [0..1]\n";
1252 let mut db = RidlDatabase::default();
1253 let loaded = load_workspace_with(
1254 &mut db,
1255 &dir.path().join("p"),
1256 &[overlay(path.clone(), text)],
1257 )
1258 .unwrap();
1259 let files = loaded.workspace.packages(&db)[0].files(&db);
1260 assert_eq!(files.len(), 1);
1261 let file = files[0];
1262 assert_eq!(file.text(&db), text);
1263 let entries: Vec<_> = loaded.sources.iter_files().collect();
1264 assert!(entries.contains(&(path_string(&path).as_str(), text)));
1265 }
1266
1267 #[test]
1268 fn an_added_file_sorts_with_the_disk_files() {
1269 let (dir, _) = overlay_fixture();
1270 let mut db = RidlDatabase::default();
1271 let loaded = load_workspace_with(
1272 &mut db,
1273 &dir.path().join("p"),
1274 &[overlay(dir.path().join("p/0.typl"), "package p\n")],
1275 )
1276 .unwrap();
1277 let files = loaded.workspace.packages(&db)[0].files(&db);
1278 assert_eq!(files.len(), 2);
1279 assert!(files[0].path(&db).ends_with("0.typl"));
1280 assert!(files[1].path(&db).ends_with("a.typl"));
1281 }
1282 #[test]
1283 fn an_overlay_replaces_rather_than_adds() {
1284 let (dir, path) = overlay_fixture();
1285 let mut db = RidlDatabase::default();
1286 let loaded = load_workspace_with(
1287 &mut db,
1288 &dir.path().join("p"),
1289 &[overlay(
1290 path,
1291 "package p\ntype Replacement: integer [0..1]\n",
1292 )],
1293 )
1294 .unwrap();
1295 assert_eq!(loaded.workspace.packages(&db)[0].files(&db).len(), 1);
1296 }
1297
1298 #[test]
1299 fn overlay_adds_a_file_to_the_package_of_its_directory() {
1300 let (dir, _) = overlay_fixture();
1301 let mut db = RidlDatabase::default();
1302 let loaded = load_workspace_with(
1303 &mut db,
1304 &dir.path().join("p"),
1305 &[overlay(dir.path().join("p/b.typl"), "package p\n")],
1306 )
1307 .unwrap();
1308 let files = loaded.workspace.packages(&db)[0].files(&db);
1309 assert_eq!(files.len(), 2);
1310 assert!(files[0].path(&db).ends_with("a.typl"));
1311 assert!(files[1].path(&db).ends_with("b.typl"));
1312 }
1313
1314 #[test]
1315 fn overlay_in_an_existing_subdirectory_joins_its_package() {
1316 let (dir, _) = overlay_fixture();
1317 dir.write("p/sub/c.typl", "package p.sub\n");
1318 let mut db = RidlDatabase::default();
1319 let loaded = load_workspace_with(
1320 &mut db,
1321 &dir.path().join("p"),
1322 &[overlay(dir.path().join("p/sub/d.typl"), "package p.sub\n")],
1323 )
1324 .unwrap();
1325 let package = loaded
1326 .workspace
1327 .packages(&db)
1328 .iter()
1329 .find(|p| p.name(&db) == "p.sub")
1330 .unwrap();
1331 let files = package.files(&db);
1332 assert_eq!(files.len(), 2);
1333 assert!(files[0].path(&db).ends_with("c.typl"));
1334 assert!(files[1].path(&db).ends_with("d.typl"));
1335 }
1336
1337 #[test]
1338 fn an_added_file_with_the_wrong_package_name_draws_typl_002() {
1339 let (dir, _) = overlay_fixture();
1340 let mut db = RidlDatabase::default();
1341 let loaded = load_workspace_with(
1342 &mut db,
1343 &dir.path().join("p"),
1344 &[overlay(dir.path().join("p/b.typl"), "package q\n")],
1345 )
1346 .unwrap();
1347 let diag = loaded
1348 .diagnostics
1349 .iter()
1350 .find(|d| d.code == DiagCode::TYPL_002)
1351 .unwrap();
1352 assert!(
1353 loaded
1354 .sources
1355 .path(diag.primary.file)
1356 .unwrap()
1357 .ends_with("b.typl")
1358 );
1359 }
1360
1361 #[test]
1362 fn an_overlay_in_a_missing_directory_is_refused() {
1363 let (dir, _) = overlay_fixture();
1364 let result = load_workspace_with(
1365 &mut RidlDatabase::default(),
1366 &dir.path().join("p"),
1367 &[overlay(
1368 dir.path().join("p/nope/e.typl"),
1369 "package p.nope\n",
1370 )],
1371 );
1372 assert!(matches!(
1373 result,
1374 Err(LoadError::OverlayOutsideWorkspace {
1375 missing_directory: true,
1376 ..
1377 })
1378 ));
1379 }
1380
1381 #[test]
1382 fn an_overlay_outside_the_workspace_is_refused() {
1383 let (dir, _) = overlay_fixture();
1384 let path = dir.write("sibling/a.typl", "package sibling\n");
1385 let result = load_workspace_with(
1386 &mut RidlDatabase::default(),
1387 &dir.path().join("p"),
1388 &[overlay(path, "package sibling\n")],
1389 );
1390 assert!(matches!(
1391 result,
1392 Err(LoadError::OverlayOutsideWorkspace {
1393 missing_directory: false,
1394 ..
1395 })
1396 ));
1397 }
1398
1399 #[test]
1400 fn an_overlay_under_a_hidden_directory_is_refused() {
1401 let (dir, _) = overlay_fixture();
1402 fs::create_dir(dir.path().join("p/.hidden")).unwrap();
1403 let result = load_workspace_with(
1404 &mut RidlDatabase::default(),
1405 &dir.path().join("p"),
1406 &[overlay(
1407 dir.path().join("p/.hidden/f.typl"),
1408 "package p.hidden\n",
1409 )],
1410 );
1411 assert!(matches!(
1412 result,
1413 Err(LoadError::OverlayOutsideWorkspace {
1414 missing_directory: false,
1415 ..
1416 })
1417 ));
1418 }
1419
1420 #[test]
1421 fn a_non_source_overlay_is_refused() {
1422 let (dir, _) = overlay_fixture();
1423 let result = load_workspace_with(
1424 &mut RidlDatabase::default(),
1425 &dir.path().join("p"),
1426 &[overlay(dir.path().join("p/ridl.toml"), "")],
1427 );
1428 assert!(matches!(result, Err(LoadError::OverlayNotSource(_))));
1429 }
1430
1431 #[test]
1432 fn single_file_mode_takes_an_overlay_for_the_entry() {
1433 let dir = TempDir::new("overlay-single");
1434 let path = dir.write("x.typl", "package x\n");
1435 let text = "package x\ntype Other: integer [0..1]\n";
1436 let mut db = RidlDatabase::default();
1437 let loaded = load_workspace_with(&mut db, &path, &[overlay(path.clone(), text)]).unwrap();
1438 assert_eq!(
1439 loaded.workspace.packages(&db)[0].files(&db)[0].text(&db),
1440 text
1441 );
1442 let result = load_workspace_with(
1443 &mut db,
1444 &path,
1445 &[overlay(dir.path().join("y.typl"), "package y\n")],
1446 );
1447 assert!(matches!(
1448 result,
1449 Err(LoadError::OverlayOutsideWorkspace {
1450 missing_directory: false,
1451 ..
1452 })
1453 ));
1454 }
1455
1456 #[cfg(unix)]
1457 #[test]
1458 fn overlay_matches_a_file_named_by_a_relative_entry() {
1459 let (dir, path) = overlay_fixture();
1460 let cwd = std::env::current_dir().unwrap();
1461 let mut relative = PathBuf::new();
1462 for part in cwd.components() {
1463 if matches!(part, std::path::Component::Normal(_)) {
1464 relative.push("..");
1465 }
1466 }
1467 relative.push(path.strip_prefix("/").unwrap());
1468 for (entry, overlay_path) in [(&relative, &path), (&path, &relative)] {
1469 let mut db = RidlDatabase::default();
1470 let text = "package p\ntype Other: integer [0..1]\n";
1471 let loaded =
1472 load_workspace_with(&mut db, entry, &[overlay(overlay_path.clone(), text)])
1473 .unwrap();
1474 assert_eq!(
1475 loaded.workspace.packages(&db)[0].files(&db)[0].text(&db),
1476 text
1477 );
1478 }
1479 drop(dir);
1480 }
1481
1482 #[test]
1483 fn load_workspace_without_overlays_is_unchanged() {
1484 let (dir, _) = overlay_fixture();
1485 let mut db = RidlDatabase::default();
1486 let first = load_workspace_with(&mut db, &dir.path().join("p"), &[]).unwrap();
1487 let describe = |db: &RidlDatabase, loaded: &LoadedWorkspace| {
1488 loaded
1489 .workspace
1490 .packages(db)
1491 .iter()
1492 .map(|p| {
1493 (
1494 p.name(db).clone(),
1495 p.files(db)
1496 .iter()
1497 .map(|f| f.path(db).clone())
1498 .collect::<Vec<_>>(),
1499 )
1500 })
1501 .collect::<Vec<_>>()
1502 };
1503 let expected = describe(&db, &first);
1504 let mut other = RidlDatabase::default();
1505 let second = load_workspace(&mut other, &dir.path().join("p")).unwrap();
1506 assert_eq!(expected, describe(&other, &second));
1507 }
1508
1509 const PACKAGE_MANIFEST: &str = "[package]\nname = \"veh.common\"\nversion = \"1.0.0\"\n";
1510
1511 #[test]
1517 fn a_package_declaring_a_reserved_name_is_refused() {
1518 const SOURCE: &str = "package ridl.std\ntype MyOwnType: m\n";
1519 let dir = TempDir::new("reserved-name");
1520 dir.write(
1521 "ridl.toml",
1522 "[package]\nname = \"ridl.std\"\nversion = \"1.0.0\"\n",
1523 );
1524 dir.write("own.typl", SOURCE);
1525
1526 let mut db = RidlDatabase::default();
1527 let loaded = load_workspace(&mut db, dir.path()).expect("the package loads");
1528 assert_eq!(
1529 codes(&loaded.diagnostics),
1530 vec!["TYPL-010"],
1531 "got: {:?}",
1532 loaded.diagnostics
1533 );
1534 let diagnostic = &loaded.diagnostics[0];
1535 assert!(
1536 diagnostic.message.contains("ridl.std"),
1537 "the message names the package: {}",
1538 diagnostic.message
1539 );
1540 assert!(
1545 diagnostic.message.contains("unreachable"),
1546 "the message states the consequence that always holds: {}",
1547 diagnostic.message
1548 );
1549 for conditional in ["artifact", "overwrit"] {
1550 assert!(
1551 !diagnostic.message.contains(conditional),
1552 "`{conditional}` is true only where the output base is the package name, \
1553 so it belongs in the catalogue entry, not the message: {}",
1554 diagnostic.message
1555 );
1556 }
1557 let declaration = SOURCE
1562 .lines()
1563 .next()
1564 .expect("the declaration is the first line");
1565 assert_eq!(
1566 (
1567 usize::from(diagnostic.primary.range.start()),
1568 usize::from(diagnostic.primary.range.end()),
1569 ),
1570 (0, declaration.len()),
1571 "reported on `{declaration}` exactly"
1572 );
1573 }
1574
1575 #[test]
1579 fn a_single_file_declaring_a_reserved_name_is_refused() {
1580 let dir = TempDir::new("reserved-name-single");
1581 let file = dir.write("ridl_std.typl", "package ridl.std\ntype MyOwnType: m\n");
1582
1583 let mut db = RidlDatabase::default();
1584 let loaded = load_workspace(&mut db, &file).expect("single-file mode loads");
1585 assert_eq!(
1586 codes(&loaded.diagnostics),
1587 vec!["TYPL-010"],
1588 "got: {:?}",
1589 loaded.diagnostics
1590 );
1591 }
1592
1593 #[test]
1596 fn only_a_compiler_provided_name_is_reserved() {
1597 let dir = TempDir::new("near-reserved");
1598 dir.write(
1599 "ridl.toml",
1600 "[package]\nname = \"ridl.stdlib\"\nversion = \"1.0.0\"\n",
1601 );
1602 dir.write("own.typl", "package ridl.stdlib\ntype MyOwnType: m\n");
1603
1604 let mut db = RidlDatabase::default();
1605 let loaded = load_workspace(&mut db, dir.path()).expect("the package loads");
1606 assert_eq!(
1607 loaded.diagnostics,
1608 Vec::new(),
1609 "`ridl.stdlib` is not a package the compiler provides"
1610 );
1611 }
1612
1613 #[test]
1617 fn two_file_package_loads_and_edit_reparses_only_the_edited_file() {
1618 let dir = TempDir::new("two-file");
1619 dir.write("ridl.toml", PACKAGE_MANIFEST);
1620 dir.write("a.typl", "package veh.common\ntype A: m\n");
1621 dir.write("b.typl", "package veh.common\ntype B: s\n");
1622
1623 let mut db = RidlDatabase::default();
1624 let loaded = load_workspace(&mut db, dir.path()).expect("the package loads");
1625 assert_eq!(
1626 loaded.diagnostics,
1627 Vec::new(),
1628 "a clean package, no diagnostics"
1629 );
1630
1631 let packages = loaded.workspace.packages(&db).clone();
1632 assert_eq!(packages.len(), 1, "one package directory, one package");
1633 assert_eq!(packages[0].name(&db).as_str(), "veh.common");
1634 assert_eq!(*packages[0].origin(&db), PackageOrigin::WorkspaceMember);
1635 assert_eq!(
1636 packages[0].imports(&db),
1637 &BTreeMap::new(),
1638 "a manifest without `[imports]` yields an empty package map",
1639 );
1640 assert_eq!(
1641 loaded.workspace.imports(&db),
1642 &BTreeMap::new(),
1643 "a standalone load leaves the workspace map empty",
1644 );
1645
1646 let files = packages[0].files(&db).clone();
1647 assert_eq!(files.len(), 2, "both .typl files load");
1648 for file in &files {
1649 assert_eq!(
1650 parse_file(&db, *file).errors(),
1651 &[],
1652 "both files parse clean"
1653 );
1654 }
1655 let a = files
1656 .iter()
1657 .copied()
1658 .find(|f| f.path(&db).ends_with("a.typl"))
1659 .expect("a.typl is loaded");
1660 let b = files
1661 .iter()
1662 .copied()
1663 .find(|f| f.path(&db).ends_with("b.typl"))
1664 .expect("b.typl is loaded");
1665
1666 db.take_executed_queries();
1669 let _ = parse_file(&db, a);
1670 let _ = parse_file(&db, b);
1671 assert_eq!(
1672 db.take_executed_queries(),
1673 Vec::new(),
1674 "re-querying unchanged inputs must run no executions",
1675 );
1676
1677 a.set_text(&mut db)
1679 .to("package veh.common\ntype A: kg\n".to_string());
1680 let _ = parse_file(&db, a);
1681 let _ = parse_file(&db, b);
1682 let executed = db.take_executed_queries();
1683 assert_eq!(
1684 executed.len(),
1685 1,
1686 "editing one file re-parses exactly one file"
1687 );
1688 assert_eq!(
1689 salsa::attach(&db, || format!("{:?}", executed[0])),
1690 format!("parse_file({:?})", a.as_id()),
1691 "the re-executed query is the parse of the edited file",
1692 );
1693 }
1694
1695 #[test]
1698 fn typl_002_on_a_mismatching_file() {
1699 let dir = TempDir::new("mismatch");
1700 dir.write("ridl.toml", PACKAGE_MANIFEST);
1701 let bad_text = "package veh.wrong\ntype B: s\n";
1702 let bad_path = dir.write("bad.typl", bad_text);
1703
1704 let mut db = RidlDatabase::default();
1705 let mut loaded = load_workspace(&mut db, dir.path()).expect("the package loads");
1706 assert_eq!(codes(&loaded.diagnostics), vec!["TYPL-002"]);
1707
1708 let diag = &loaded.diagnostics[0];
1709 assert_eq!(diag.severity, Severity::Error);
1710 assert_eq!(
1711 diag.primary.range,
1712 byte_range(0, "package veh.wrong".len()),
1713 "the primary span is the mismatching `package` line",
1714 );
1715 assert_eq!(
1716 diag.primary.file,
1717 loaded.sources.file_id(&path_string(&bad_path), bad_text),
1718 "the span points into the mismatching file",
1719 );
1720
1721 let packages = loaded.workspace.packages(&db).clone();
1723 assert_eq!(packages.len(), 1);
1724 assert_eq!(packages[0].files(&db).len(), 1);
1725 }
1726
1727 #[test]
1730 fn typl_001_on_a_double_package_declaration() {
1731 let dir = TempDir::new("double-decl");
1732 dir.write("ridl.toml", PACKAGE_MANIFEST);
1733 dir.write(
1734 "dup.typl",
1735 "package veh.common\npackage veh.extra\ntype A: m\n",
1736 );
1737
1738 let mut db = RidlDatabase::default();
1739 let loaded = load_workspace(&mut db, dir.path()).expect("the package loads");
1740 assert_eq!(codes(&loaded.diagnostics), vec!["TYPL-001"]);
1741 assert_eq!(
1742 loaded.diagnostics[0].primary.range,
1743 byte_range(19, 36),
1744 "the primary span is the second `package` declaration",
1745 );
1746 }
1747
1748 #[test]
1753 fn workspace_mode_scopes_imports_per_package() {
1754 let dir = TempDir::new("workspace");
1755 dir.write(
1756 "ridl.toml",
1757 "[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",
1758 );
1759 dir.write(
1760 "m-one/ridl.toml",
1761 "[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",
1762 );
1763 dir.write("m-one/one.typl", "package veh.one\ntype A: m\n");
1764 dir.write(
1765 "m-two/ridl.toml",
1766 "[package]\nname = \"veh.two\"\nversion = \"1.0.0\"\n\n[imports]\n\"two.only\" = \"https://registry.example.com/two/only@v1.0.0\"\n",
1767 );
1768 dir.write("m-two/two.typl", "package veh.two\ntype B: s\n");
1769
1770 let mut db = RidlDatabase::default();
1771 let loaded = load_workspace(&mut db, dir.path()).expect("the workspace loads");
1772 assert_eq!(loaded.diagnostics, Vec::new(), "a clean workspace");
1773
1774 let packages = loaded.workspace.packages(&db).clone();
1775 let names: Vec<String> = packages.iter().map(|p| p.name(&db).clone()).collect();
1776 assert_eq!(
1777 names,
1778 vec!["veh.one", "veh.two"],
1779 "both members load, in member order"
1780 );
1781
1782 let workspace_imports = loaded.workspace.imports(&db).clone();
1785 assert_eq!(
1786 workspace_imports.get("third.dep").map(String::as_str),
1787 Some("https://registry.example.com/third/dep@v1.0.0"),
1788 "the workspace map keeps the root pin, not the member pin",
1789 );
1790 assert_eq!(
1791 workspace_imports.get("shared.util").map(String::as_str),
1792 Some("https://registry.example.com/shared/util@v1.0.0"),
1793 );
1794 assert_eq!(workspace_imports.len(), 2, "no member entry leaks upward");
1795
1796 let one_imports = packages[0].imports(&db).clone();
1798 assert_eq!(
1799 one_imports.get("third.dep").map(String::as_str),
1800 Some("https://mirror.example.com/third/dep@v2.0.0"),
1801 "the member's own pin shadows the workspace default for it alone",
1802 );
1803 assert_eq!(
1804 one_imports.get("member.only").map(String::as_str),
1805 Some("https://registry.example.com/member/only@v1.0.0"),
1806 );
1807 assert!(
1808 !one_imports.contains_key("two.only"),
1809 "a sibling's pin never leaks into another member",
1810 );
1811 assert_eq!(one_imports.len(), 2, "no workspace entry is merged in");
1812
1813 let two_imports = packages[1].imports(&db).clone();
1814 assert_eq!(
1815 two_imports.get("two.only").map(String::as_str),
1816 Some("https://registry.example.com/two/only@v1.0.0"),
1817 );
1818 assert!(
1819 !two_imports.contains_key("member.only"),
1820 "the sibling's pin never leaks into this member",
1821 );
1822 assert!(
1823 !two_imports.contains_key("third.dep"),
1824 "neither the root default nor the sibling's pin is merged in",
1825 );
1826 assert_eq!(two_imports.len(), 1);
1827 }
1828
1829 #[test]
1833 fn defaults_timing_precedence_package_shadows_workspace() {
1834 let dir = TempDir::new("defaults-timing");
1835 dir.write(
1836 "ridl.toml",
1837 "[workspace]\nmembers = [\"m-own\", \"m-inherit\"]\n\n[defaults]\ntiming = \"[100ms..1000ms]\"\n",
1838 );
1839 dir.write(
1841 "m-own/ridl.toml",
1842 "[package]\nname = \"veh.own\"\nversion = \"1.0.0\"\n\n[defaults]\ntiming = \"[50ms..2s]\"\n",
1843 );
1844 dir.write("m-own/own.typl", "package veh.own\ntype A: m\n");
1845 dir.write(
1847 "m-inherit/ridl.toml",
1848 "[package]\nname = \"veh.inherit\"\nversion = \"1.0.0\"\n",
1849 );
1850 dir.write("m-inherit/inherit.typl", "package veh.inherit\ntype B: s\n");
1851
1852 let mut db = RidlDatabase::default();
1853 let loaded = load_workspace(&mut db, dir.path()).expect("the workspace loads");
1854 assert_eq!(loaded.diagnostics, Vec::new(), "a clean workspace");
1855
1856 let packages = loaded.workspace.packages(&db).clone();
1857 let own = packages
1858 .iter()
1859 .find(|p| p.name(&db) == "veh.own")
1860 .expect("m-own loads");
1861 assert_eq!(
1862 own.defaults(&db).timing.as_deref(),
1863 Some("[50ms..2s]"),
1864 "the member's own `[defaults]` shadows the workspace default",
1865 );
1866 let inherit = packages
1867 .iter()
1868 .find(|p| p.name(&db) == "veh.inherit")
1869 .expect("m-inherit loads");
1870 assert_eq!(
1871 inherit.defaults(&db).timing.as_deref(),
1872 Some("[100ms..1000ms]"),
1873 "a member without `[defaults]` inherits the workspace default",
1874 );
1875 }
1876
1877 #[test]
1880 fn defaults_precedence_is_per_key() {
1881 let dir = TempDir::new("defaults-per-key");
1882 dir.write(
1883 "ridl.toml",
1884 "[workspace]\nmembers = [\"m\"]\n\n[defaults]\ncommand_timing = \"[..2s]\"\nquery_timing = \"[..4s]\"\n",
1885 );
1886 dir.write(
1887 "m/ridl.toml",
1888 "[package]\nname = \"veh.m\"\nversion = \"1.0.0\"\n\n[defaults]\nquery_timing = \"[..5s]\"\n",
1889 );
1890 dir.write("m/m.typl", "package veh.m\ntype A: m\n");
1891
1892 let mut db = RidlDatabase::default();
1893 let loaded = load_workspace(&mut db, dir.path()).expect("the workspace loads");
1894 assert_eq!(loaded.diagnostics, Vec::new(), "a clean workspace");
1895 let packages = loaded.workspace.packages(&db).clone();
1896 let member = packages
1897 .iter()
1898 .find(|p| p.name(&db) == "veh.m")
1899 .expect("the member loads");
1900 let defaults = member.defaults(&db);
1901 assert_eq!(defaults.command_timing.as_deref(), Some("[..2s]"));
1902 assert_eq!(defaults.query_timing.as_deref(), Some("[..5s]"));
1903 assert_eq!(defaults.timing, None);
1904 }
1905
1906 #[test]
1910 fn defaults_precedence_is_per_key_in_both_directions() {
1911 let dir = TempDir::new("defaults-per-key-reverse");
1912 dir.write(
1913 "ridl.toml",
1914 "[workspace]\nmembers = [\"m\"]\n\n[defaults]\ncommand_timing = \"[..2s]\"\nquery_timing = \"[..4s]\"\n",
1915 );
1916 dir.write(
1917 "m/ridl.toml",
1918 "[package]\nname = \"veh.m\"\nversion = \"1.0.0\"\n\n[defaults]\ncommand_timing = \"[..7s]\"\n",
1919 );
1920 dir.write("m/m.typl", "package veh.m\ntype A: m\n");
1921
1922 let mut db = RidlDatabase::default();
1923 let loaded = load_workspace(&mut db, dir.path()).expect("the workspace loads");
1924 assert_eq!(loaded.diagnostics, Vec::new(), "a clean workspace");
1925 let packages = loaded.workspace.packages(&db).clone();
1926 let member = packages
1927 .iter()
1928 .find(|p| p.name(&db) == "veh.m")
1929 .expect("the member loads");
1930 let defaults = member.defaults(&db);
1931 assert_eq!(
1932 defaults.command_timing.as_deref(),
1933 Some("[..7s]"),
1934 "the member's own `command_timing` wins over the workspace's",
1935 );
1936 assert_eq!(
1937 defaults.query_timing.as_deref(),
1938 Some("[..4s]"),
1939 "a member without `query_timing` inherits the workspace's",
1940 );
1941 assert_eq!(defaults.timing, None);
1942 }
1943
1944 #[test]
1947 fn standalone_rpc_defaults_ride_on_the_tree() {
1948 let dir = TempDir::new("standalone-rpc-defaults");
1949 dir.write(
1950 "ridl.toml",
1951 "[package]\nname = \"veh.common\"\nversion = \"1.0.0\"\n\n[defaults]\ncommand_timing = \"[..2s]\"\nquery_timing = \"[5ms..4s]\"\n",
1952 );
1953 dir.write("a.typl", "package veh.common\ntype A: m\n");
1954 dir.write("sub/s.typl", "package veh.common.sub\ntype S: s\n");
1955
1956 let mut db = RidlDatabase::default();
1957 let loaded = load_workspace(&mut db, dir.path()).expect("the package tree loads");
1958 assert_eq!(loaded.diagnostics, Vec::new());
1959 let packages = loaded.workspace.packages(&db).clone();
1960 let names: Vec<_> = packages.iter().map(|p| p.name(&db).clone()).collect();
1961 assert!(
1962 names.iter().any(|name| name == "veh.common")
1963 && names.iter().any(|name| name == "veh.common.sub"),
1964 "the root and the nested package both load: {names:?}",
1965 );
1966 for package in &packages {
1967 let defaults = package.defaults(&db);
1968 assert_eq!(
1969 defaults.command_timing.as_deref(),
1970 Some("[..2s]"),
1971 "{}",
1972 package.name(&db),
1973 );
1974 assert_eq!(
1975 defaults.query_timing.as_deref(),
1976 Some("[5ms..4s]"),
1977 "{}",
1978 package.name(&db),
1979 );
1980 }
1981 }
1982
1983 #[test]
1986 fn standalone_defaults_timing_rides_on_the_tree() {
1987 let dir = TempDir::new("standalone-defaults");
1988 dir.write(
1989 "ridl.toml",
1990 "[package]\nname = \"veh.common\"\nversion = \"1.0.0\"\n\n[defaults]\ntiming = \"[20ms..200ms]\"\n",
1991 );
1992 dir.write("a.typl", "package veh.common\ntype A: m\n");
1993 dir.write("sub/s.typl", "package veh.common.sub\ntype S: s\n");
1994
1995 let mut db = RidlDatabase::default();
1996 let loaded = load_workspace(&mut db, dir.path()).expect("the package tree loads");
1997 assert_eq!(loaded.diagnostics, Vec::new());
1998 for package in loaded.workspace.packages(&db) {
1999 assert_eq!(
2000 package.defaults(&db).timing.as_deref(),
2001 Some("[20ms..200ms]"),
2002 "every package in the tree carries the manifest default",
2003 );
2004 }
2005
2006 let bare = TempDir::new("standalone-defaults-bare");
2008 let single = bare.write("iface.ridl", "package solo\ntype A: m\n");
2009 let mut single_db = RidlDatabase::default();
2010 let single_loaded =
2011 load_workspace(&mut single_db, &single).expect("single-file mode loads");
2012 assert_eq!(
2013 single_loaded.workspace.packages(&single_db)[0].defaults(&single_db),
2014 &TimingDefaults::default(),
2015 "single-file mode carries no configured default",
2016 );
2017 }
2018
2019 #[test]
2026 fn lint_scopes_follow_root_then_member() {
2027 let dir = TempDir::new("lint-scopes");
2028 dir.write(
2029 "ridl.toml",
2030 "[workspace]\nmembers = [\"a\", \"b\"]\n\n[lints]\nmissing-timing = \"deny\"\nshared-error-type = \"allow\"\n",
2031 );
2032 dir.write(
2033 "a/ridl.toml",
2034 "[package]\nname = \"a\"\nversion = \"1.0.0\"\n\n[lints]\nmissing-timing = \"warn\"\n",
2035 );
2036 let a_file = dir.write("a/a.typl", "package a\ntype A: m\n");
2037 dir.write(
2038 "b/ridl.toml",
2039 "[package]\nname = \"b\"\nversion = \"1.0.0\"\n",
2040 );
2041 let b_file = dir.write("b/b.typl", "package b\ntype B: s\n");
2042
2043 let mut db = RidlDatabase::default();
2044 let loaded = load_workspace(&mut db, dir.path()).expect("the workspace loads");
2045 assert_eq!(loaded.diagnostics, Vec::new(), "a clean workspace");
2046
2047 let missing_timing = lint_by_name("missing-timing").expect("missing-timing is a lint");
2048 let shared_error_type =
2049 lint_by_name("shared-error-type").expect("shared-error-type is a lint");
2050 let levels = |path: &Path| {
2051 loaded
2052 .lints
2053 .for_path(path)
2054 .unwrap_or_else(|| panic!("`{}` is in a scope", path.display()))
2055 };
2056
2057 let in_a = levels(&a_file);
2058 assert_eq!(in_a.level(missing_timing), Some(LintLevel::Warn));
2059 assert_eq!(in_a.level(shared_error_type), Some(LintLevel::Allow));
2060 assert_eq!(levels(&b_file).level(missing_timing), Some(LintLevel::Deny));
2061 let root = levels(&dir.path().join("ridl.toml"));
2062 assert_eq!(root.level(missing_timing), Some(LintLevel::Deny));
2063 assert_eq!(root.level(shared_error_type), Some(LintLevel::Allow));
2064 assert_eq!(
2065 levels(&dir.path().join("a/ridl.toml")).level(missing_timing),
2066 Some(LintLevel::Warn),
2067 );
2068
2069 let packages = loaded.workspace.packages(&db).clone();
2072 let a = packages
2073 .iter()
2074 .find(|p| p.name(&db) == "a")
2075 .expect("member a loads");
2076 let recorded = a.files(&db)[0].path(&db).clone();
2077 assert_eq!(
2078 levels(Path::new(&recorded)).level(missing_timing),
2079 Some(LintLevel::Warn),
2080 "the recorded path `{recorded}` resolves to the member's scope",
2081 );
2082 }
2083
2084 #[test]
2088 fn a_member_manifest_diagnostic_is_in_the_member_scope() {
2089 let dir = TempDir::new("lint-scope-manifest");
2090 dir.write(
2091 "ridl.toml",
2092 "[workspace]\nmembers = [\"a\"]\n\n[lints]\nunknown-lint = \"deny\"\n",
2093 );
2094 dir.write(
2095 "a/ridl.toml",
2096 "[package]\nname = \"a\"\nversion = \"1.0.0\"\n\n[lints]\nunknown-lint = \"allow\"\nnope = \"warn\"\n",
2097 );
2098 dir.write("a/a.typl", "package a\ntype A: m\n");
2099
2100 let mut db = RidlDatabase::default();
2101 let loaded = load_workspace(&mut db, dir.path()).expect("the workspace loads");
2102 assert_eq!(codes(&loaded.diagnostics), vec!["MANI-010"]);
2103 let unknown_lint = lint_by_name("unknown-lint").expect("unknown-lint is a lint");
2104 let path = loaded
2105 .sources
2106 .path(loaded.diagnostics[0].primary.file)
2107 .expect("the manifest has a path");
2108 assert_eq!(
2109 loaded
2110 .lints
2111 .for_path(Path::new(path))
2112 .expect("the member manifest is in a scope")
2113 .level(unknown_lint),
2114 Some(LintLevel::Allow),
2115 "the recorded manifest path `{path}` resolves to the member's scope",
2116 );
2117
2118 let mut diagnostics = loaded.diagnostics.clone();
2119 apply_lint_levels(&mut diagnostics, &loaded.sources, &loaded.lints);
2120 assert_eq!(
2121 diagnostics,
2122 Vec::new(),
2123 "`unknown-lint = \"allow\"` silences the MANI-010"
2124 );
2125 }
2126
2127 #[test]
2130 fn standalone_lint_scope_covers_the_tree_and_single_file_has_none() {
2131 let dir = TempDir::new("lint-scope-standalone");
2132 dir.write(
2133 "ridl.toml",
2134 "[package]\nname = \"veh.common\"\nversion = \"1.0.0\"\n\n[lints]\nmissing-timing = \"deny\"\n",
2135 );
2136 let top = dir.write("a.typl", "package veh.common\ntype A: m\n");
2137 let nested = dir.write("sub/s.typl", "package veh.common.sub\ntype S: s\n");
2138
2139 let mut db = RidlDatabase::default();
2140 let loaded = load_workspace(&mut db, dir.path()).expect("the package tree loads");
2141 assert_eq!(loaded.diagnostics, Vec::new());
2142 let missing_timing = lint_by_name("missing-timing").expect("missing-timing is a lint");
2143 for path in [&top, &nested] {
2144 assert_eq!(
2145 loaded
2146 .lints
2147 .for_path(path)
2148 .unwrap_or_else(|| panic!("`{}` is in a scope", path.display()))
2149 .level(missing_timing),
2150 Some(LintLevel::Deny),
2151 );
2152 }
2153
2154 let bare = TempDir::new("lint-scope-bare");
2155 let single = bare.write("iface.ridl", "package solo\ntype A: m\n");
2156 let mut single_db = RidlDatabase::default();
2157 let single_loaded =
2158 load_workspace(&mut single_db, &single).expect("single-file mode loads");
2159 assert!(
2160 single_loaded.lints.for_path(&single).is_none(),
2161 "single-file mode has no lint scope",
2162 );
2163 }
2164
2165 #[test]
2168 fn mani_004_on_a_nested_workspace_member() {
2169 let dir = TempDir::new("nested");
2170 dir.write(
2171 "ridl.toml",
2172 "[workspace]\nmembers = [\"m-bad\", \"m-good\"]\n",
2173 );
2174 let bad_text = "[workspace]\nmembers = []\n";
2175 let bad_path = dir.write("m-bad/ridl.toml", bad_text);
2176 dir.write(
2177 "m-good/ridl.toml",
2178 "[package]\nname = \"veh.good\"\nversion = \"1.0.0\"\n",
2179 );
2180 dir.write("m-good/good.typl", "package veh.good\ntype A: m\n");
2181
2182 let mut db = RidlDatabase::default();
2183 let mut loaded = load_workspace(&mut db, dir.path()).expect("the workspace loads");
2184 assert_eq!(codes(&loaded.diagnostics), vec!["MANI-004"]);
2185
2186 let diag = &loaded.diagnostics[0];
2187 assert_eq!(diag.severity, Severity::Error);
2188 assert_eq!(
2189 diag.primary.file,
2190 loaded.sources.file_id(&path_string(&bad_path), bad_text),
2191 "the span points into the member's own manifest",
2192 );
2193 assert_eq!(
2194 diag.primary.range,
2195 byte_range(0, "[workspace]".len()),
2196 "the span is the `[workspace]` section header",
2197 );
2198
2199 let packages = loaded.workspace.packages(&db).clone();
2200 let names: Vec<String> = packages.iter().map(|p| p.name(&db).clone()).collect();
2201 assert_eq!(names, vec!["veh.good"], "the nested member loads nothing");
2202 }
2203
2204 #[test]
2207 fn mani_008_on_a_missing_member_directory() {
2208 let dir = TempDir::new("missing-member");
2209 dir.write(
2210 "ridl.toml",
2211 "[workspace]\nmembers = [\"m-gone\", \"m-good\"]\n",
2212 );
2213 dir.write(
2214 "m-good/ridl.toml",
2215 "[package]\nname = \"veh.good\"\nversion = \"1.0.0\"\n",
2216 );
2217 dir.write("m-good/good.typl", "package veh.good\ntype A: m\n");
2218
2219 let mut db = RidlDatabase::default();
2220 let loaded = load_workspace(&mut db, dir.path()).expect("the workspace loads");
2221 assert_eq!(codes(&loaded.diagnostics), vec!["MANI-008"]);
2222 assert_eq!(loaded.diagnostics[0].severity, Severity::Error);
2223 assert!(loaded.diagnostics[0].message.contains("m-gone"));
2224
2225 let packages = loaded.workspace.packages(&db).clone();
2226 assert_eq!(packages.len(), 1);
2227 assert_eq!(packages[0].name(&db).as_str(), "veh.good");
2228 }
2229
2230 #[test]
2234 fn a_subdirectory_is_its_own_package_named_by_its_path() {
2235 let dir = TempDir::new("subdir");
2236 dir.write(
2237 "ridl.toml",
2238 "[package]\nname = \"veh.common\"\nversion = \"1.0.0\"\n\n[imports]\n\"some.dep\" = \"https://registry.example.com/some/dep@v1.0.0\"\n",
2239 );
2240 dir.write("a.typl", "package veh.common\ntype A: m\n");
2241 dir.write("types/t.typl", "package veh.common.types\ntype T: s\n");
2242
2243 let mut db = RidlDatabase::default();
2244 let loaded = load_workspace(&mut db, dir.path()).expect("the package tree loads");
2245 assert_eq!(loaded.diagnostics, Vec::new());
2246
2247 let packages = loaded.workspace.packages(&db).clone();
2248 let names: Vec<String> = packages.iter().map(|p| p.name(&db).clone()).collect();
2249 assert_eq!(names, vec!["veh.common", "veh.common.types"]);
2250 for package in &packages {
2251 assert_eq!(
2252 package.imports(&db).get("some.dep").map(String::as_str),
2253 Some("https://registry.example.com/some/dep@v1.0.0"),
2254 "every package in the manifest's tree carries its `[imports]`",
2255 );
2256 }
2257 assert_eq!(
2258 loaded.workspace.imports(&db),
2259 &BTreeMap::new(),
2260 "a standalone load leaves the workspace map empty",
2261 );
2262 }
2263
2264 #[test]
2268 fn a_package_directory_mixes_typl_and_ridl_files() {
2269 let dir = TempDir::new("mixed");
2270 dir.write("ridl.toml", PACKAGE_MANIFEST);
2271 dir.write("a.typl", "package veh.common\ntype A: m\n");
2272 dir.write("b.ridl", "package veh.common\ntype B: s\n");
2273
2274 let mut db = RidlDatabase::default();
2275 let loaded = load_workspace(&mut db, dir.path()).expect("the package loads");
2276 assert_eq!(loaded.diagnostics, Vec::new(), "a clean mixed package");
2277
2278 let packages = loaded.workspace.packages(&db).clone();
2279 assert_eq!(packages.len(), 1, "one package directory, one package");
2280 let files = packages[0].files(&db).clone();
2281 assert_eq!(files.len(), 2, "both the .typl and the .ridl file load");
2282 for file in &files {
2283 assert_eq!(
2284 parse_file(&db, *file).errors(),
2285 &[],
2286 "both files parse clean"
2287 );
2288 }
2289 assert!(files.iter().any(|f| f.path(&db).ends_with("a.typl")));
2290 assert!(files.iter().any(|f| f.path(&db).ends_with("b.ridl")));
2291 }
2292
2293 #[test]
2297 fn a_package_directory_loads_its_rsdl_files() {
2298 let dir = TempDir::new("rsdl");
2299 dir.write("ridl.toml", PACKAGE_MANIFEST);
2300 dir.write("a.typl", "package veh.common\ntype A: m\n");
2301 dir.write("b.ridl", "package veh.common\ntype B: s\n");
2302 dir.write("c.rsdl", "package veh.common\n");
2303
2304 let mut db = RidlDatabase::default();
2305 let loaded = load_workspace(&mut db, dir.path()).expect("the package loads");
2306 assert_eq!(loaded.diagnostics, Vec::new(), "a clean mixed package");
2307
2308 let packages = loaded.workspace.packages(&db).clone();
2309 assert_eq!(packages.len(), 1, "one package directory, one package");
2310 let files = packages[0].files(&db).clone();
2311 let paths: Vec<&str> = files.iter().map(|f| f.path(&db).as_str()).collect();
2312 assert_eq!(files.len(), 3, "the .rsdl file loads too: {paths:?}");
2313 let rsdl = files
2314 .iter()
2315 .find(|f| f.path(&db).ends_with("c.rsdl"))
2316 .expect("the .rsdl file is a package file");
2317 assert_eq!(
2318 crate::db::profile_of_path(rsdl.path(&db)),
2319 ridl_syntax::Profile::Rsdl
2320 );
2321 assert_eq!(
2322 parse_file(&db, *rsdl).errors(),
2323 &[],
2324 "the .rsdl file parses clean"
2325 );
2326 }
2327
2328 #[test]
2330 fn single_file_mode_accepts_a_bare_ridl_entry() {
2331 let dir = TempDir::new("single-ridl");
2332 let path = dir.write("iface.ridl", "package veh.iface\ntype A: m\n");
2333
2334 let mut db = RidlDatabase::default();
2335 let loaded = load_workspace(&mut db, &path).expect("single-file mode loads");
2336 assert_eq!(loaded.diagnostics, Vec::new(), "exempt from TYPL-002");
2337
2338 let packages = loaded.workspace.packages(&db).clone();
2339 assert_eq!(packages.len(), 1, "one synthetic package");
2340 assert_eq!(
2341 packages[0].name(&db).as_str(),
2342 "veh.iface",
2343 "named from the file's declared package",
2344 );
2345 }
2346
2347 #[test]
2348 fn every_package_of_a_tree_carries_the_manifest_name_as_its_unit() {
2349 let dir = TempDir::new("unit-tree");
2350 dir.write(
2351 "ridl.toml",
2352 "[package]\nname = \"veh.hmi\"\nversion = \"1.0.0\"\n",
2353 );
2354 dir.write("hmi.ridl", "package veh.hmi\ntype A: m\n");
2355 dir.write("cluster/speed.ridl", "package veh.hmi.cluster\ntype S: m\n");
2356
2357 let mut db = RidlDatabase::default();
2358 let loaded = load_workspace(&mut db, dir.path()).expect("the package tree loads");
2359 assert_eq!(loaded.diagnostics, Vec::new());
2360
2361 let packages = loaded.workspace.packages(&db).clone();
2362 assert_eq!(packages.len(), 2);
2363 for package in &packages {
2364 assert_eq!(package.unit(&db).as_str(), "veh.hmi");
2365 }
2366 assert_eq!(
2367 loaded.units,
2368 BTreeMap::from([("veh.hmi".to_string(), dir.path().to_path_buf())]),
2369 );
2370 }
2371
2372 #[test]
2373 fn a_workspace_member_is_a_unit_and_the_root_is_not() {
2374 let dir = TempDir::new("unit-members");
2375 dir.write("ridl.toml", "[workspace]\nmembers = [\"a\", \"b\"]\n");
2376 dir.write(
2377 "a/ridl.toml",
2378 "[package]\nname = \"x.a\"\nversion = \"1.0.0\"\n",
2379 );
2380 dir.write("a/a.ridl", "package x.a\ntype A: m\n");
2381 dir.write(
2382 "b/ridl.toml",
2383 "[package]\nname = \"x.b\"\nversion = \"1.0.0\"\n",
2384 );
2385 dir.write("b/b.ridl", "package x.b\ntype B: m\n");
2386
2387 let mut db = RidlDatabase::default();
2388 let loaded = load_workspace(&mut db, dir.path()).expect("the workspace loads");
2389 assert_eq!(loaded.diagnostics, Vec::new());
2390
2391 assert_eq!(
2392 loaded.units,
2393 BTreeMap::from([
2394 ("x.a".to_string(), dir.path().join("a")),
2395 ("x.b".to_string(), dir.path().join("b")),
2396 ]),
2397 );
2398 let units: Vec<String> = loaded
2399 .workspace
2400 .packages(&db)
2401 .iter()
2402 .map(|p| p.unit(&db).clone())
2403 .collect();
2404 assert_eq!(units, vec!["x.a", "x.b"]);
2405 }
2406
2407 #[test]
2408 fn a_single_file_is_its_own_unit() {
2409 let dir = TempDir::new("unit-single");
2410 let path = dir.write("p.ridl", "package p\ntype A: m\n");
2411
2412 let mut db = RidlDatabase::default();
2413 let loaded = load_workspace(&mut db, &path).expect("single-file mode loads");
2414 assert_eq!(loaded.diagnostics, Vec::new());
2415
2416 let packages = loaded.workspace.packages(&db).clone();
2417 assert_eq!(packages.len(), 1);
2418 assert_eq!(packages[0].unit(&db).as_str(), "p");
2419 assert_eq!(
2420 loaded.units,
2421 BTreeMap::from([("p".to_string(), dir.path().to_path_buf())]),
2422 );
2423 }
2424
2425 #[cfg(unix)]
2428 #[test]
2429 fn a_symlinked_directory_is_not_followed() {
2430 let dir = TempDir::new("symlink");
2431 dir.write("ridl.toml", PACKAGE_MANIFEST);
2432 dir.write("a.typl", "package veh.common\ntype A: m\n");
2433 std::os::unix::fs::symlink(dir.path(), dir.path().join("loop"))
2435 .expect("create the directory symlink");
2436
2437 let mut db = RidlDatabase::default();
2438 let loaded = load_workspace(&mut db, dir.path()).expect("the package loads");
2439 assert_eq!(loaded.diagnostics, Vec::new());
2440
2441 let packages = loaded.workspace.packages(&db).clone();
2442 assert_eq!(packages.len(), 1, "the symlink cycle adds no packages");
2443 assert_eq!(packages[0].name(&db).as_str(), "veh.common");
2444 }
2445
2446 #[test]
2450 fn a_non_utf8_file_is_reported_and_skipped() {
2451 let dir = TempDir::new("non-utf8");
2452 dir.write("ridl.toml", PACKAGE_MANIFEST);
2453 dir.write("a.typl", "package veh.common\ntype A: m\n");
2454 fs::write(dir.path().join("bad.typl"), [0xFF, 0xFE, 0x00, 0x9F])
2455 .expect("write the non-UTF8 fixture");
2456
2457 let mut db = RidlDatabase::default();
2458 let loaded = load_workspace(&mut db, dir.path()).expect("the load continues");
2459 assert_eq!(loaded.diagnostics.len(), 1);
2460 assert!(
2461 loaded.diagnostics[0].message.contains("UTF-8"),
2462 "the diagnostic names the encoding problem",
2463 );
2464
2465 let packages = loaded.workspace.packages(&db).clone();
2466 assert_eq!(packages.len(), 1);
2467 let files = packages[0].files(&db).clone();
2468 assert_eq!(files.len(), 1, "only the valid file loads");
2469 assert!(files[0].path(&db).ends_with("a.typl"));
2470 }
2471
2472 #[test]
2476 fn single_file_mode_loads_the_e0_fixture() {
2477 let fixture = concat!(
2478 env!("CARGO_MANIFEST_DIR"),
2479 "/../ridl-syntax/fixtures/walking_skeleton.typl",
2480 );
2481 let text = fs::read_to_string(fixture).expect("the E0 fixture exists");
2482 assert!(
2483 text.contains("package fixtures"),
2484 "the fixture declares `package fixtures`",
2485 );
2486
2487 let dir = TempDir::new("single-file");
2491 let path = dir.write("walking_skeleton.typl", &text);
2492
2493 let mut db = RidlDatabase::default();
2494 let loaded = load_workspace(&mut db, &path).expect("single-file mode loads");
2495 assert_eq!(loaded.diagnostics, Vec::new(), "exempt from TYPL-002");
2496
2497 let packages = loaded.workspace.packages(&db).clone();
2498 assert_eq!(packages.len(), 1, "one synthetic package");
2499 assert_eq!(
2500 packages[0].name(&db).as_str(),
2501 "fixtures",
2502 "named from the file's declared package",
2503 );
2504 assert_eq!(*packages[0].origin(&db), PackageOrigin::WorkspaceMember);
2505
2506 let files = packages[0].files(&db).clone();
2507 assert_eq!(files.len(), 1);
2508 assert_eq!(
2509 parse_file(&db, files[0]).errors(),
2510 &[],
2511 "the fixture parses clean"
2512 );
2513 }
2514
2515 const LOCK_TEXT: &str = "\
2518# interfaces.lock — written by ridl lock; do not edit by hand.
2519next 3
2520Cabin 1
2521service:veh.common.climate 2
2522";
2523
2524 #[test]
2528 fn a_lock_beside_the_sources_rides_on_the_package() {
2529 let dir = TempDir::new("lock");
2530 dir.write("ridl.toml", PACKAGE_MANIFEST);
2531 dir.write("a.ridl", "package veh.common\ntype A: m\n");
2532 let lock_path = dir.write("interfaces.lock", LOCK_TEXT);
2533
2534 let mut db = RidlDatabase::default();
2535 let loaded = load_workspace(&mut db, dir.path()).expect("the package loads");
2536 assert_eq!(
2537 loaded.diagnostics,
2538 Vec::new(),
2539 "a well-formed lock draws nothing"
2540 );
2541
2542 let packages = loaded.workspace.packages(&db).clone();
2543 let lock = packages[0]
2544 .lock(&db)
2545 .as_ref()
2546 .expect("the lock rides on the package");
2547 assert_eq!(lock.path, path_string(&lock_path));
2548 assert_eq!(lock.text, LOCK_TEXT);
2549 assert_eq!(lock.lock.next, 3);
2550 assert_eq!(lock.lock.entries.len(), 2);
2551 assert_eq!(
2552 lock.lock.entries[1].key,
2553 crate::interface_lock::LockKey::Service("veh.common.climate".to_string())
2554 );
2555 }
2556
2557 #[test]
2558 fn a_package_with_no_lock_has_none() {
2559 let dir = TempDir::new("no-lock");
2560 dir.write("ridl.toml", PACKAGE_MANIFEST);
2561 dir.write("a.ridl", "package veh.common\ntype A: m\n");
2562
2563 let mut db = RidlDatabase::default();
2564 let loaded = load_workspace(&mut db, dir.path()).expect("the package loads");
2565 assert_eq!(loaded.diagnostics, Vec::new());
2566 let packages = loaded.workspace.packages(&db).clone();
2567 assert_eq!(*packages[0].lock(&db), None);
2568 }
2569
2570 #[test]
2574 fn a_malformed_lock_is_ridl_410_on_its_own_line() {
2575 let dir = TempDir::new("bad-lock");
2576 dir.write("ridl.toml", PACKAGE_MANIFEST);
2577 dir.write("a.ridl", "package veh.common\ntype A: m\n");
2578 let text = "# interfaces.lock — written by ridl lock; do not edit by hand.\n\
2579 next 2\n\
2580 Cabin 1\n\
2581 Door 1\n";
2582 let lock_path = dir.write("interfaces.lock", text);
2583
2584 let mut db = RidlDatabase::default();
2585 let loaded = load_workspace(&mut db, dir.path()).expect("the package loads");
2586 assert_eq!(codes(&loaded.diagnostics), ["RIDL-410"]);
2587 let diagnostic = &loaded.diagnostics[0];
2588 assert_eq!(diagnostic.severity, Severity::Error);
2589 assert_eq!(
2590 loaded.sources.path(diagnostic.primary.file),
2591 Some(path_string(&lock_path).as_str()),
2592 "the span is in the lock file"
2593 );
2594 assert_eq!(loaded.sources.text(diagnostic.primary.file), Some(text));
2595 let line_start = text
2596 .find("Door 1")
2597 .expect("the offending line is in the text");
2598 assert_eq!(
2599 diagnostic.primary.range,
2600 byte_range(line_start, line_start + "Door 1".len()),
2601 "the span is the offending line"
2602 );
2603 assert_eq!(
2604 diagnostic.message,
2605 "`interfaces.lock` is malformed: number 1 is on two entries: `Cabin` and `Door` — \
2606 resolve the conflict or restore the file from version control, then run `ridl lock`"
2607 );
2608
2609 let packages = loaded.workspace.packages(&db).clone();
2610 assert_eq!(
2611 *packages[0].lock(&db),
2612 None,
2613 "a malformed lock does not ride on the package"
2614 );
2615 }
2616
2617 #[test]
2620 fn an_empty_lock_is_ridl_410_at_the_start_of_the_file() {
2621 let dir = TempDir::new("empty-lock");
2622 dir.write("ridl.toml", PACKAGE_MANIFEST);
2623 dir.write("a.ridl", "package veh.common\ntype A: m\n");
2624 dir.write("interfaces.lock", "");
2625
2626 let mut db = RidlDatabase::default();
2627 let loaded = load_workspace(&mut db, dir.path()).expect("the package loads");
2628 assert_eq!(codes(&loaded.diagnostics), ["RIDL-410"]);
2629 assert_eq!(loaded.diagnostics[0].primary.range, byte_range(0, 0));
2630 assert!(
2631 loaded.diagnostics[0].message.contains("no `next` line"),
2632 "got: {}",
2633 loaded.diagnostics[0].message
2634 );
2635 }
2636
2637 #[test]
2640 fn a_lock_that_is_not_utf8_is_ridl_410() {
2641 let dir = TempDir::new("binary-lock");
2642 dir.write("ridl.toml", PACKAGE_MANIFEST);
2643 dir.write("a.ridl", "package veh.common\ntype A: m\n");
2644 fs::write(dir.path().join("interfaces.lock"), [0xff, 0xfe, b'\n'])
2645 .expect("write the bytes");
2646
2647 let mut db = RidlDatabase::default();
2648 let loaded = load_workspace(&mut db, dir.path()).expect("the package loads");
2649 assert_eq!(codes(&loaded.diagnostics), ["RIDL-410"]);
2650 assert!(
2651 loaded.diagnostics[0].message.contains("not valid UTF-8"),
2652 "got: {}",
2653 loaded.diagnostics[0].message
2654 );
2655 assert_eq!(loaded.diagnostics[0].primary.range, byte_range(0, 0));
2656 let packages = loaded.workspace.packages(&db).clone();
2657 assert_eq!(*packages[0].lock(&db), None);
2658 }
2659
2660 #[test]
2663 fn single_file_mode_reads_the_lock_beside_the_file() {
2664 let dir = TempDir::new("single-lock");
2665 let path = dir.write("iface.ridl", "package veh.iface\ntype A: m\n");
2666 dir.write("interfaces.lock", LOCK_TEXT);
2667
2668 let mut db = RidlDatabase::default();
2669 let loaded = load_workspace(&mut db, &path).expect("single-file mode loads");
2670 assert_eq!(loaded.diagnostics, Vec::new());
2671 let packages = loaded.workspace.packages(&db).clone();
2672 let lock = packages[0]
2673 .lock(&db)
2674 .as_ref()
2675 .expect("the lock rides on the synthetic package");
2676 assert_eq!(lock.lock.next, 3);
2677 }
2678
2679 #[test]
2682 fn every_package_of_a_unit_carries_the_lock_of_the_manifest_directory() {
2683 let dir = TempDir::new("unit-lock");
2684 dir.write(
2685 "ridl.toml",
2686 "[package]\nname = \"veh.hmi\"\nversion = \"1.0.0\"\n",
2687 );
2688 dir.write("hmi.ridl", "package veh.hmi\ntype A: m\n");
2689 let lock_path = dir.write("interfaces.lock", "next 1\n");
2690 dir.write("cluster/speed.ridl", "package veh.hmi.cluster\ntype B: m\n");
2691
2692 let mut db = RidlDatabase::default();
2693 let loaded = load_workspace(&mut db, dir.path()).expect("the tree loads");
2694 assert_eq!(loaded.diagnostics, Vec::new());
2695 let packages = loaded.workspace.packages(&db).clone();
2696 assert_eq!(packages.len(), 2);
2697 for package in &packages {
2698 let lock = package
2699 .lock(&db)
2700 .as_ref()
2701 .unwrap_or_else(|| panic!("`{}` has the unit's lock", package.name(&db)));
2702 assert_eq!(lock.path, path_string(&lock_path));
2703 }
2704 }
2705
2706 #[test]
2709 fn a_lock_in_a_subdirectory_is_not_read() {
2710 let dir = TempDir::new("subdir-lock-ignored");
2711 dir.write(
2712 "ridl.toml",
2713 "[package]\nname = \"veh.hmi\"\nversion = \"1.0.0\"\n",
2714 );
2715 dir.write("hmi.ridl", "package veh.hmi\ntype A: m\n");
2716 dir.write("cluster/speed.ridl", "package veh.hmi.cluster\ntype B: m\n");
2717 let ignored = dir.write("cluster/interfaces.lock", "x");
2718
2719 let mut db = RidlDatabase::default();
2720 let loaded = load_workspace(&mut db, dir.path()).expect("the tree loads");
2721 assert_eq!(codes(&loaded.diagnostics), vec!["RIDL-416"]);
2722 let diag = &loaded.diagnostics[0];
2723 assert_eq!(diag.severity, Severity::Warning);
2724 assert_eq!(
2725 loaded.sources.path(diag.primary.file),
2726 Some(path_string(&ignored).as_str())
2727 );
2728 assert_eq!(diag.primary.range, byte_range(0, 0));
2729 assert!(diag.message.contains("unit `veh.hmi`"), "{}", diag.message);
2730 let packages = loaded.workspace.packages(&db).clone();
2731 assert_eq!(packages.len(), 2);
2732 for package in &packages {
2733 assert_eq!(*package.lock(&db), None);
2734 }
2735 }
2736
2737 fn compiled_paths(db: &RidlDatabase, loaded: &LoadedWorkspace) -> Vec<String> {
2739 loaded
2740 .workspace
2741 .packages(db)
2742 .iter()
2743 .flat_map(|package| package.files(db).iter().map(|file| file.path(db).clone()))
2744 .collect()
2745 }
2746
2747 #[test]
2750 fn a_rxdl_file_is_reported_and_not_compiled() {
2751 let dir = TempDir::new("rxdl-on-disk");
2752 dir.write(
2753 "ridl.toml",
2754 "[package]\nname = \"veh.hmi\"\nversion = \"1.0.0\"\n",
2755 );
2756 dir.write("hmi.ridl", "package veh.hmi\ntype A: m\n");
2757 let top = dir.write("hmi.rxdl", "package veh.hmi\ntype B: m\n");
2758 let beside = dir.write("menu.rxdl", "package veh.hmi\ntype C: m\n");
2759 let nested = dir.write("cluster/speed.rxdl", "package veh.hmi.cluster\n");
2760
2761 let mut db = RidlDatabase::default();
2762 let loaded = load_workspace(&mut db, dir.path()).expect("the tree loads");
2763 assert_eq!(
2764 codes(&loaded.diagnostics),
2765 vec!["RIDL-417", "RIDL-417", "RIDL-417"]
2766 );
2767 let mut anchored: Vec<_> = loaded
2768 .diagnostics
2769 .iter()
2770 .map(|diag| {
2771 assert_eq!(diag.severity, Severity::Warning);
2772 assert_eq!(diag.primary.range, byte_range(0, 0));
2773 loaded.sources.path(diag.primary.file).map(str::to_string)
2774 })
2775 .collect();
2776 anchored.sort();
2777 let mut expected = vec![
2778 Some(path_string(&nested)),
2779 Some(path_string(&top)),
2780 Some(path_string(&beside)),
2781 ];
2782 expected.sort();
2783 assert_eq!(anchored, expected);
2784 assert_eq!(
2785 compiled_paths(&db, &loaded),
2786 vec![path_string(&dir.path().join("hmi.ridl"))]
2787 );
2788 }
2789
2790 #[test]
2793 fn a_rxdl_overlay_is_reported_and_not_compiled() {
2794 let (dir, typl) = overlay_fixture();
2795 let on_disk = dir.write("p/b.rxdl", "package p\n");
2796 let added = dir.path().join("p/c.rxdl");
2797 let mut db = RidlDatabase::default();
2798 let result = load_workspace_with(
2799 &mut db,
2800 &dir.path().join("p"),
2801 &[
2802 overlay(on_disk.clone(), "package p\n"),
2803 overlay(added.clone(), "package p\n"),
2804 ],
2805 );
2806 assert!(
2807 result.is_ok(),
2808 "a `.rxdl` overlay is accepted: {:?}",
2809 result.err()
2810 );
2811 let loaded = result.unwrap();
2812 assert_eq!(codes(&loaded.diagnostics), vec!["RIDL-417", "RIDL-417"]);
2813 let anchored: Vec<_> = loaded
2814 .diagnostics
2815 .iter()
2816 .map(|diag| loaded.sources.path(diag.primary.file).map(str::to_string))
2817 .collect();
2818 assert_eq!(
2819 anchored,
2820 vec![Some(path_string(&on_disk)), Some(path_string(&added))]
2821 );
2822 assert_eq!(compiled_paths(&db, &loaded), vec![path_string(&typl)]);
2823 }
2824
2825 #[test]
2828 fn a_rmdl_file_is_reported_and_not_compiled() {
2829 let (dir, typl) = overlay_fixture();
2830 let on_disk = dir.write("p/b.rmdl", "package p\n");
2831 let added = dir.path().join("p/c.rmdl");
2832 let mut db = RidlDatabase::default();
2833 let loaded = load_workspace_with(
2834 &mut db,
2835 &dir.path().join("p"),
2836 &[overlay(added.clone(), "package p\n")],
2837 )
2838 .expect("a `.rmdl` overlay is accepted");
2839 assert_eq!(codes(&loaded.diagnostics), vec!["RIDL-417", "RIDL-417"]);
2840 let anchored: Vec<_> = loaded
2841 .diagnostics
2842 .iter()
2843 .map(|diag| {
2844 assert_eq!(diag.severity, Severity::Warning);
2845 assert!(
2846 diag.message.contains("is a `.rmdl` file; the rmdl profile"),
2847 "{}",
2848 diag.message
2849 );
2850 loaded.sources.path(diag.primary.file).map(str::to_string)
2851 })
2852 .collect();
2853 assert_eq!(
2854 anchored,
2855 vec![Some(path_string(&on_disk)), Some(path_string(&added))]
2856 );
2857 assert_eq!(compiled_paths(&db, &loaded), vec![path_string(&typl)]);
2858 }
2859
2860 #[test]
2863 fn a_rxdl_overlay_interns_the_overlay_text() {
2864 let (dir, _) = overlay_fixture();
2865 let on_disk = dir.write("p/b.rxdl", "package p\n");
2866 let text = "package p\n// unsaved\n";
2867 let mut db = RidlDatabase::default();
2868 let loaded = load_workspace_with(
2869 &mut db,
2870 &dir.path().join("p"),
2871 &[overlay(on_disk.clone(), text)],
2872 )
2873 .expect("the overlay is accepted");
2874 assert_eq!(codes(&loaded.diagnostics), vec!["RIDL-417"]);
2875 assert_eq!(
2876 loaded.sources.text(loaded.diagnostics[0].primary.file),
2877 Some(text)
2878 );
2879 }
2880
2881 #[test]
2884 fn a_rxdl_file_on_disk_interns_no_text() {
2885 let (dir, _) = overlay_fixture();
2886 dir.write("p/b.rxdl", "package p\n");
2887 let mut db = RidlDatabase::default();
2888 let loaded = load_workspace(&mut db, &dir.path().join("p")).expect("the tree loads");
2889 assert_eq!(codes(&loaded.diagnostics), vec!["RIDL-417"]);
2890 assert_eq!(
2891 loaded.sources.text(loaded.diagnostics[0].primary.file),
2892 Some("")
2893 );
2894 }
2895
2896 #[test]
2900 fn a_rxdl_or_rmdl_entry_in_a_package_directory_is_a_warning() {
2901 for extension in ["rxdl", "rmdl"] {
2902 let (dir, typl) = overlay_fixture();
2903 let entry = dir.write(&format!("p/b.{extension}"), "package p\n");
2904 let mut db = RidlDatabase::default();
2905 let loaded = load_workspace(&mut db, &entry).expect("the package loads");
2906 assert_eq!(codes(&loaded.diagnostics), vec!["RIDL-417"]);
2907 assert_eq!(
2908 loaded.sources.path(loaded.diagnostics[0].primary.file),
2909 Some(path_string(&entry).as_str())
2910 );
2911 assert_eq!(compiled_paths(&db, &loaded), vec![path_string(&typl)]);
2912 }
2913 }
2914
2915 #[test]
2920 fn ridl_417_is_reported_in_path_order() {
2921 let (dir, _) = overlay_fixture();
2922 let z = dir.write("p/z.rxdl", "package p\n");
2923 let y = dir.write("p/y.rmdl", "package p\n");
2924 let c = dir.path().join("p/c.rxdl");
2925 let b = dir.path().join("p/b.rmdl");
2926 let a = dir.path().join("p/a.rxdl");
2927 let mut db = RidlDatabase::default();
2928 let loaded = load_workspace_with(
2929 &mut db,
2930 &dir.path().join("p"),
2931 &[
2932 overlay(c.clone(), "package p\n"),
2933 overlay(b.clone(), "package p\n"),
2934 overlay(a.clone(), "package p\n"),
2935 ],
2936 )
2937 .expect("the overlays are accepted");
2938 let anchored: Vec<_> = loaded
2939 .diagnostics
2940 .iter()
2941 .map(|diag| loaded.sources.path(diag.primary.file).map(str::to_string))
2942 .collect();
2943 assert_eq!(
2944 anchored,
2945 [&a, &b, &c, &y, &z]
2946 .map(|path| Some(path_string(path)))
2947 .to_vec()
2948 );
2949 }
2950
2951 #[test]
2956 fn a_single_rxdl_or_rmdl_file_is_a_load_error() {
2957 for extension in ["rxdl", "rmdl"] {
2958 let dir = TempDir::new("unsupported-single");
2959 let path = dir.write(&format!("hmi.{extension}"), "package veh.hmi\n");
2960
2961 let mut db = RidlDatabase::default();
2962 let error = load_workspace(&mut db, &path)
2963 .err()
2964 .unwrap_or_else(|| panic!("a lone `.{extension}` entry is a load error"));
2965 assert_eq!(error.kind(), io::ErrorKind::InvalidInput);
2966 let message = error.to_string();
2967 assert!(
2968 message.contains(&format!("`.{extension}` is not a supported")),
2969 "{message}"
2970 );
2971 }
2972 }
2973
2974 #[test]
2977 fn a_single_rxdl_or_rmdl_overlay_is_a_load_error() {
2978 for extension in ["rxdl", "rmdl"] {
2979 let dir = TempDir::new("unsupported-single-overlay");
2980 let path = dir.write(&format!("hmi.{extension}"), "package veh.hmi\n");
2981
2982 let mut db = RidlDatabase::default();
2983 let result = load_workspace_with(
2984 &mut db,
2985 &path,
2986 &[overlay(path.clone(), "package veh.hmi\ntype B: m\n")],
2987 );
2988 let Err(LoadError::Io(error)) = result else {
2989 panic!("a lone `.{extension}` entry with an overlay is a load error");
2990 };
2991 assert_eq!(error.kind(), io::ErrorKind::InvalidInput);
2992 assert!(
2993 error
2994 .to_string()
2995 .contains(&format!("`.{extension}` is not a supported")),
2996 "{error}"
2997 );
2998 }
2999 }
3000
3001 const PACKAGE_A: &str = "[package]\nname = \"a\"\nversion = \"1.0.0\"\n";
3004 const PACKAGE_B: &str = "[package]\nname = \"b\"\nversion = \"1.0.0\"\n";
3005
3006 #[test]
3007 fn find_root_walks_from_a_member_to_its_workspace() {
3008 let dir = TempDir::new("find-root-member");
3009 dir.write("ridl.toml", "[workspace]\nmembers = [\"a\", \"b\"]\n");
3010 dir.write("a/ridl.toml", PACKAGE_A);
3011 dir.write("a/src/a.typl", "package a.src\n");
3012 dir.write("b/ridl.toml", PACKAGE_B);
3013
3014 assert_eq!(
3015 find_root(&dir.path().join("a/src")),
3016 Some(dir.path().to_path_buf())
3017 );
3018 assert_eq!(
3019 find_root(&dir.path().join("a")),
3020 Some(dir.path().to_path_buf())
3021 );
3022 }
3023
3024 #[test]
3026 fn find_root_normalises_the_member_path() {
3027 let dir = TempDir::new("find-root-normalise");
3028 dir.write("ridl.toml", "[workspace]\nmembers = [\"./a/\"]\n");
3029 dir.write("a/ridl.toml", PACKAGE_A);
3030
3031 assert_eq!(
3032 find_root(&dir.path().join("a")),
3033 Some(dir.path().to_path_buf())
3034 );
3035 }
3036
3037 #[test]
3038 fn find_root_keeps_an_unlisted_package_standalone() {
3039 let dir = TempDir::new("find-root-unlisted");
3040 dir.write("ridl.toml", "[workspace]\nmembers = [\"b\"]\n");
3041 dir.write("a/ridl.toml", PACKAGE_A);
3042 dir.write("b/ridl.toml", PACKAGE_B);
3043
3044 assert_eq!(find_root(&dir.path().join("a")), Some(dir.path().join("a")));
3045 }
3046
3047 #[test]
3048 fn find_root_stops_at_the_first_workspace() {
3049 let dir = TempDir::new("find-root-first-workspace");
3050 dir.write("ridl.toml", "[workspace]\nmembers = [\"inner/a\"]\n");
3051 dir.write("inner/ridl.toml", "[workspace]\nmembers = [\"b\"]\n");
3052 dir.write("inner/a/ridl.toml", PACKAGE_A);
3053
3054 assert_eq!(
3055 find_root(&dir.path().join("inner/a")),
3056 Some(dir.path().join("inner/a"))
3057 );
3058 }
3059
3060 #[test]
3061 fn find_root_stops_at_a_git_directory() {
3062 let dir = TempDir::new("find-root-git");
3063 dir.write("ridl.toml", "[workspace]\nmembers = [\"repo/a\"]\n");
3064 fs::create_dir_all(dir.path().join("repo/.git")).expect("create .git");
3065 dir.write("repo/a/ridl.toml", PACKAGE_A);
3066
3067 assert_eq!(
3068 find_root(&dir.path().join("repo/a")),
3069 Some(dir.path().join("repo/a"))
3070 );
3071 }
3072
3073 #[test]
3076 fn find_root_checks_the_manifest_beside_a_git_directory() {
3077 let dir = TempDir::new("find-root-git-root");
3078 dir.write("ridl.toml", "[workspace]\nmembers = [\"a\"]\n");
3079 fs::create_dir_all(dir.path().join(".git")).expect("create .git");
3080 dir.write("a/ridl.toml", PACKAGE_A);
3081
3082 assert_eq!(
3083 find_root(&dir.path().join("a")),
3084 Some(dir.path().to_path_buf())
3085 );
3086 }
3087
3088 #[test]
3091 fn find_root_returns_an_unreadable_manifest_above_the_package() {
3092 let dir = TempDir::new("find-root-unreadable");
3093 fs::write(dir.path().join("ridl.toml"), [0xff, 0xfe]).expect("write the manifest");
3094 dir.write("a/ridl.toml", PACKAGE_A);
3095
3096 assert_eq!(
3097 find_root(&dir.path().join("a")),
3098 Some(dir.path().to_path_buf())
3099 );
3100 }
3101
3102 #[test]
3106 fn up_keeps_the_relative_form_of_the_entry() {
3107 assert_eq!(up(Path::new("members/a"), 2), PathBuf::from("."));
3108 assert_eq!(up(Path::new("a"), 1), PathBuf::from("."));
3109 assert_eq!(up(Path::new("a"), 2), PathBuf::from(".."));
3110 assert_eq!(up(Path::new("."), 2), PathBuf::from("../.."));
3111 assert_eq!(up(Path::new(""), 1), PathBuf::from(".."));
3112 assert_eq!(up(Path::new("a/.."), 1), PathBuf::from("a/../.."));
3113 assert_eq!(up(Path::new("/w/a"), 1), PathBuf::from("/w"));
3114 }
3115
3116 #[test]
3117 fn member_entry_sets_report_scope() {
3118 let dir = TempDir::new("report-scope");
3119 dir.write("ridl.toml", "[workspace]\nmembers = [\"a\", \"b\"]\n");
3120 dir.write("a/ridl.toml", PACKAGE_A);
3121 dir.write("a/a.typl", "package a\ntype A: integer [0..1]\n");
3122 dir.write("b/ridl.toml", PACKAGE_B);
3123 let b_file = dir.write("b/b.typl", "package b\ntype B: integer [0..1]\n");
3124
3125 let mut db = RidlDatabase::default();
3126 let loaded = load_workspace(&mut db, &dir.path().join("a")).expect("the workspace loads");
3127 assert_eq!(loaded.report_scope, Some(dir.path().join("a")));
3128 let mut names: Vec<_> = loaded
3129 .workspace
3130 .packages(&db)
3131 .iter()
3132 .map(|p| p.name(&db).clone())
3133 .collect();
3134 names.sort();
3135 assert_eq!(names, vec!["a", "b"], "both members compile");
3136
3137 let mut db = RidlDatabase::default();
3138 let loaded = load_workspace(&mut db, &b_file).expect("the workspace loads");
3139 assert_eq!(loaded.report_scope, Some(dir.path().join("b")));
3140
3141 let mut db = RidlDatabase::default();
3142 let loaded = load_workspace(&mut db, dir.path()).expect("the workspace loads");
3143 assert_eq!(loaded.report_scope, None, "the root reports on everything");
3144 }
3145
3146 #[test]
3147 fn codegen_header_is_read_and_normalised() {
3148 let dir = TempDir::new("codegen-header");
3149 dir.write(
3150 "ridl.toml",
3151 &format!("{PACKAGE_A}\n[codegen]\nheader-file = \"H.txt\"\n"),
3152 );
3153 dir.write("a.typl", "package a\ntype A: integer [0..1]\n");
3154 dir.write("H.txt", "SPDX-License-Identifier: MIT\r\n");
3155 let mut db = RidlDatabase::default();
3156 let loaded = load_workspace(&mut db, dir.path()).expect("the package loads");
3157 assert!(loaded.diagnostics.is_empty(), "{:?}", loaded.diagnostics);
3158 assert_eq!(
3159 loaded.codegen_header.as_deref(),
3160 Some("SPDX-License-Identifier: MIT")
3161 );
3162 }
3163
3164 #[test]
3165 fn codegen_header_file_missing_is_mani_011() {
3166 let dir = TempDir::new("codegen-header-missing");
3167 let manifest = format!("{PACKAGE_A}\n[codegen]\nheader-file = \"nope.txt\"\n");
3168 dir.write("ridl.toml", &manifest);
3169 dir.write("a.typl", "package a\ntype A: integer [0..1]\n");
3170 let mut db = RidlDatabase::default();
3171 let loaded = load_workspace(&mut db, dir.path()).expect("the package loads");
3172 assert_eq!(codes(&loaded.diagnostics), vec!["MANI-011"]);
3173 let diag = &loaded.diagnostics[0];
3174 let start = usize::from(diag.primary.range.start());
3175 let end = usize::from(diag.primary.range.end());
3176 assert_eq!(&manifest[start..end], "\"nope.txt\"");
3177 let resolved = dir.path().join("nope.txt");
3178 assert!(
3179 diag.message.contains(&resolved.display().to_string()),
3180 "{}",
3181 diag.message
3182 );
3183 assert_eq!(loaded.codegen_header, None);
3184 }
3185
3186 #[test]
3187 fn codegen_header_file_that_is_not_utf8_is_mani_011() {
3188 let dir = TempDir::new("codegen-header-not-utf8");
3189 dir.write(
3190 "ridl.toml",
3191 &format!("{PACKAGE_A}\n[codegen]\nheader-file = \"H.txt\"\n"),
3192 );
3193 dir.write("a.typl", "package a\ntype A: integer [0..1]\n");
3194 fs::write(dir.path().join("H.txt"), [0xffu8, 0xfe]).expect("write the header");
3195 let mut db = RidlDatabase::default();
3196 let loaded = load_workspace(&mut db, dir.path()).expect("the package loads");
3197 assert_eq!(codes(&loaded.diagnostics), vec!["MANI-011"]);
3198 assert!(
3199 loaded.diagnostics[0].message.contains("H.txt"),
3200 "{}",
3201 loaded.diagnostics[0].message
3202 );
3203 assert_eq!(loaded.codegen_header, None);
3204 }
3205
3206 #[test]
3207 fn codegen_header_file_with_a_control_character_is_mani_011() {
3208 let dir = TempDir::new("codegen-header-control");
3209 dir.write(
3210 "ridl.toml",
3211 &format!("{PACKAGE_A}\n[codegen]\nheader-file = \"H.txt\"\n"),
3212 );
3213 dir.write("a.typl", "package a\ntype A: integer [0..1]\n");
3214 dir.write("H.txt", "A\u{2028}B\u{0}\n");
3215 let mut db = RidlDatabase::default();
3216 let loaded = load_workspace(&mut db, dir.path()).expect("the package loads");
3217 assert_eq!(codes(&loaded.diagnostics), vec!["MANI-011"]);
3218 let diag = &loaded.diagnostics[0];
3219 let manifest = fs::read_to_string(dir.path().join("ridl.toml")).unwrap();
3220 let start = usize::from(diag.primary.range.start());
3221 let end = usize::from(diag.primary.range.end());
3222 assert_eq!(&manifest[start..end], "\"H.txt\"");
3223 let message = &diag.message;
3224 assert!(message.contains("control character"), "{message}");
3225 assert!(message.contains("U+2028"), "{message}");
3226 assert!(
3227 message.contains(&dir.path().join("H.txt").display().to_string()),
3228 "{message}"
3229 );
3230 assert_eq!(loaded.codegen_header, None);
3231 }
3232
3233 #[test]
3234 fn codegen_header_file_in_a_member_is_mani_012() {
3235 let dir = TempDir::new("codegen-header-member");
3236 dir.write(
3237 "ridl.toml",
3238 "[workspace]\nmembers = [\"a\"]\n\n[codegen]\nheader-file = \"H.txt\"\n",
3239 );
3240 dir.write("H.txt", "root header\n");
3241 let member = format!("{PACKAGE_A}\n[codegen]\nheader-file = \"M.txt\"\n");
3242 dir.write("a/ridl.toml", &member);
3243 dir.write("a/a.typl", "package a\ntype A: integer [0..1]\n");
3244 let mut db = RidlDatabase::default();
3245 let loaded = load_workspace(&mut db, dir.path()).expect("the workspace loads");
3246 assert_eq!(codes(&loaded.diagnostics), vec!["MANI-012"]);
3247 let diag = &loaded.diagnostics[0];
3248 let start = usize::from(diag.primary.range.start());
3249 let end = usize::from(diag.primary.range.end());
3250 assert_eq!(&member[start..end], "\"M.txt\"");
3251 assert_eq!(loaded.codegen_header.as_deref(), Some("root header"));
3252 }
3253
3254 #[test]
3255 fn a_manifest_below_a_unit_is_mani_013_and_its_tree_is_not_loaded() {
3256 let dir = TempDir::new("nested-manifest");
3257 dir.write(
3258 "ridl.toml",
3259 "[package]\nname = \"veh.hmi\"\nversion = \"1.0.0\"\n",
3260 );
3261 dir.write("hmi.ridl", "package veh.hmi\n");
3262 let nested = dir.write(
3263 "cluster/ridl.toml",
3264 "[package]\nname = \"veh.hmi.cluster\"\nversion = \"1.0.0\"\n",
3265 );
3266 dir.write("cluster/x.ridl", "package veh.hmi.cluster\n");
3267 let mut db = RidlDatabase::default();
3268 let loaded = load_workspace(&mut db, dir.path()).expect("the unit loads");
3269 assert_eq!(codes(&loaded.diagnostics), vec!["MANI-013"]);
3270 let diag = &loaded.diagnostics[0];
3271 assert_eq!(
3272 loaded.sources.path(diag.primary.file),
3273 Some(path_string(&nested).as_str())
3274 );
3275 assert_eq!(diag.primary.range, byte_range(0, 0));
3276 assert!(diag.message.contains("unit `veh.hmi`"), "{}", diag.message);
3277 let packages = loaded.workspace.packages(&db);
3278 assert_eq!(packages.len(), 1);
3279 assert_eq!(packages[0].name(&db), "veh.hmi");
3280 }
3281
3282 #[test]
3283 fn a_member_whose_tree_holds_another_member_is_mani_013() {
3284 let dir = TempDir::new("nested-member");
3285 dir.write("ridl.toml", "[workspace]\nmembers = [\"a\", \"a/b\"]\n");
3286 dir.write(
3287 "a/ridl.toml",
3288 "[package]\nname = \"a\"\nversion = \"1.0.0\"\n",
3289 );
3290 dir.write("a/a.typl", "package a\n");
3291 dir.write(
3292 "a/b/ridl.toml",
3293 "[package]\nname = \"b\"\nversion = \"1.0.0\"\n",
3294 );
3295 dir.write("a/b/b.typl", "package b\n");
3296 let mut db = RidlDatabase::default();
3297 let loaded = load_workspace(&mut db, dir.path()).expect("the workspace loads");
3298 let count = codes(&loaded.diagnostics)
3299 .iter()
3300 .filter(|c| **c == "MANI-013")
3301 .count();
3302 assert_eq!(count, 1, "{:?}", codes(&loaded.diagnostics));
3303 }
3304
3305 #[test]
3306 fn a_root_package_already_claimed_by_a_sibling_tree_is_mani_014() {
3307 let dir = TempDir::new("claimed-twice");
3308 dir.write("ridl.toml", "[workspace]\nmembers = [\"base\", \"hmi\"]\n");
3309 dir.write(
3310 "base/ridl.toml",
3311 "[package]\nname = \"com.example\"\nversion = \"1.0.0\"\n",
3312 );
3313 dir.write("base/hmi/x.ridl", "package com.example.hmi\n");
3314 let second = "[package]\nname = \"com.example.hmi\"\nversion = \"1.0.0\"\n";
3315 let second_path = dir.write("hmi/ridl.toml", second);
3316 dir.write("hmi/y.ridl", "package com.example.hmi\n");
3317 let mut db = RidlDatabase::default();
3318 let loaded = load_workspace(&mut db, dir.path()).expect("the workspace loads");
3319 assert_eq!(codes(&loaded.diagnostics), vec!["MANI-014"]);
3320 let diag = &loaded.diagnostics[0];
3321 assert_eq!(
3322 loaded.sources.path(diag.primary.file),
3323 Some(path_string(&second_path).as_str())
3324 );
3325 let start = usize::from(diag.primary.range.start());
3326 let end = usize::from(diag.primary.range.end());
3327 assert_eq!(&second[start..end], "\"com.example.hmi\"");
3328 let order = format!(
3329 "already declared by unit `com.example` (`{}`); unit `com.example.hmi` declares it too, in `{}`",
3330 dir.path().join("base").display(),
3331 dir.path().join("hmi").display()
3332 );
3333 assert!(diag.message.contains(&order), "{}", diag.message);
3334 let packages = loaded.workspace.packages(&db);
3335 let claimed: Vec<_> = packages
3336 .iter()
3337 .filter(|p| p.name(&db) == "com.example.hmi")
3338 .collect();
3339 assert_eq!(claimed.len(), 1);
3340 assert_eq!(claimed[0].unit(&db), "com.example");
3341 }
3342
3343 fn two_members_with_one_name(members: &str, first: &str, second: &str) {
3348 let dir = TempDir::new("one-name-twice");
3349 dir.write("ridl.toml", &format!("[workspace]\nmembers = {members}\n"));
3350 let manifest = "[package]\nname = \"x\"\nversion = \"1.0.0\"\n";
3351 dir.write(&format!("{first}/ridl.toml"), manifest);
3352 dir.write(
3353 &format!("{first}/x.ridl"),
3354 &format!("package x\n\ninterface {}A {{}}\n", first.to_uppercase()),
3355 );
3356 let second_path = dir.write(&format!("{second}/ridl.toml"), manifest);
3357 dir.write(
3358 &format!("{second}/x.ridl"),
3359 &format!("package x\n\ninterface {}A {{}}\n", second.to_uppercase()),
3360 );
3361 let mut db = RidlDatabase::default();
3362 let loaded = load_workspace(&mut db, dir.path()).expect("the workspace loads");
3363 assert_eq!(codes(&loaded.diagnostics), vec!["MANI-014"]);
3364 let diag = &loaded.diagnostics[0];
3365 assert_eq!(
3366 loaded.sources.path(diag.primary.file),
3367 Some(path_string(&second_path).as_str())
3368 );
3369 let start = usize::from(diag.primary.range.start());
3370 let end = usize::from(diag.primary.range.end());
3371 assert_eq!(&manifest[start..end], "\"x\"");
3372 let order = format!(
3373 "source package `x` is already declared by the unit in `{}`; the unit in `{}` declares it too",
3374 dir.path().join(first).display(),
3375 dir.path().join(second).display()
3376 );
3377 assert!(diag.message.contains(&order), "{}", diag.message);
3378 assert_eq!(loaded.units.get("x"), Some(&dir.path().join(first)));
3379 let packages = loaded.workspace.packages(&db);
3380 assert_eq!(packages.len(), 1);
3381 let files = packages[0].files(&db);
3382 assert_eq!(files.len(), 1);
3383 assert!(
3384 files[0]
3385 .text(&db)
3386 .contains(&format!("{}A", first.to_uppercase())),
3387 "the package holds `{first}`'s file"
3388 );
3389 }
3390
3391 #[test]
3392 fn two_members_with_one_name_are_mani_014_on_the_second() {
3393 two_members_with_one_name("[\"a\", \"b\"]", "a", "b");
3394 }
3395
3396 #[test]
3397 fn two_members_with_one_name_are_mani_014_on_the_second_in_reverse_order() {
3398 two_members_with_one_name("[\"b\", \"a\"]", "b", "a");
3399 }
3400
3401 #[test]
3406 fn two_directories_of_one_unit_naming_one_package_are_not_mani_014() {
3407 let dir = TempDir::new("one-unit-one-name");
3408 dir.write("ridl.toml", "[workspace]\nmembers = [\"x\"]\n");
3409 dir.write(
3410 "x/ridl.toml",
3411 "[package]\nname = \"x\"\nversion = \"1.0.0\"\n",
3412 );
3413 dir.write("x/a.b/p.ridl", "package x.a.b\n\ninterface P {}\n");
3414 dir.write("x/a/b/q.ridl", "package x.a.b\n\ninterface Q {}\n");
3415 let mut db = RidlDatabase::default();
3416 let loaded = load_workspace(&mut db, dir.path()).expect("the workspace loads");
3417 let codes = codes(&loaded.diagnostics);
3418 assert!(!codes.contains(&"MANI-014"), "{codes:?}");
3419 }
3420
3421 #[test]
3422 fn a_member_listed_twice_is_loaded_once() {
3423 member_listed_twice_is_loaded_once("[\"a\", \"a\"]");
3424 }
3425
3426 #[test]
3429 fn a_member_listed_twice_through_a_parent_step_is_loaded_once() {
3430 member_listed_twice_is_loaded_once("[\"a\", \"b/../a\"]");
3431 }
3432
3433 fn member_listed_twice_is_loaded_once(members: &str) {
3440 let (codes, named_x) = load_member_a(members);
3441 assert!(!codes.contains(&"MANI-014".to_string()), "{codes:?}");
3442 let mani_005 = codes.iter().filter(|c| *c == "MANI-005").count();
3443 assert_eq!(mani_005, 1, "{codes:?}");
3444 assert_eq!(named_x, 1);
3445 let (listed_once, _) = load_member_a("[\"a\"]");
3446 assert_eq!(codes, listed_once);
3447 }
3448
3449 fn load_member_a(members: &str) -> (Vec<String>, usize) {
3454 let dir = TempDir::new("member-twice");
3455 std::fs::create_dir_all(dir.path().join("b")).expect("create `b`");
3456 dir.write("ridl.toml", &format!("[workspace]\nmembers = {members}\n"));
3457 dir.write(
3458 "a/ridl.toml",
3459 "[package]\nname = \"x\"\nversion = \"1.0.0\"\nunknown = 1\n",
3460 );
3461 dir.write("a/x.ridl", "package x\n\ninterface A {}\n");
3462 let mut db = RidlDatabase::default();
3463 let loaded = load_workspace(&mut db, dir.path()).expect("the workspace loads");
3464 for (i, diag) in loaded.diagnostics.iter().enumerate() {
3465 assert!(
3466 !loaded.diagnostics[..i].contains(diag),
3467 "{} appears twice",
3468 diag.code.as_str()
3469 );
3470 }
3471 let packages = loaded.workspace.packages(&db);
3472 let named_x = packages.iter().filter(|p| p.name(&db) == "x").count();
3473 let codes = codes(&loaded.diagnostics)
3474 .into_iter()
3475 .map(str::to_string)
3476 .collect();
3477 (codes, named_x)
3478 }
3479
3480 #[test]
3481 fn two_units_with_a_shared_prefix_and_no_overlap_both_load() {
3482 let dir = TempDir::new("shared-prefix");
3483 dir.write("ridl.toml", "[workspace]\nmembers = [\"base\", \"hmi\"]\n");
3484 dir.write(
3485 "base/ridl.toml",
3486 "[package]\nname = \"com.example\"\nversion = \"1.0.0\"\n",
3487 );
3488 dir.write("base/ids.typl", "package com.example\n");
3489 dir.write(
3490 "hmi/ridl.toml",
3491 "[package]\nname = \"com.example.hmi\"\nversion = \"1.0.0\"\n",
3492 );
3493 dir.write("hmi/y.ridl", "package com.example.hmi\n");
3494 let mut db = RidlDatabase::default();
3495 let loaded = load_workspace(&mut db, dir.path()).expect("the workspace loads");
3496 assert!(
3497 loaded.diagnostics.is_empty(),
3498 "{:?}",
3499 codes(&loaded.diagnostics)
3500 );
3501 let units: std::collections::BTreeSet<String> = loaded
3502 .workspace
3503 .packages(&db)
3504 .iter()
3505 .map(|p| p.unit(&db).to_string())
3506 .collect();
3507 assert_eq!(
3508 units,
3509 ["com.example", "com.example.hmi"].map(String::from).into()
3510 );
3511 }
3512}