From a3d98437a45dc18ffe322ee934bfca421c5178a0 Mon Sep 17 00:00:00 2001 From: Mine Starks <16928427+minestarks@users.noreply.github.com> Date: Sat, 18 Jan 2025 00:50:23 +0000 Subject: [PATCH] fix language service panic when file is under the project folder but not in the files list --- compiler/qsc_project/src/project.rs | 152 +- compiler/qsc_project/src/tests.rs | 10 +- compiler/qsc_project/src/tests/harness.rs | 9 +- language_service/src/state/tests.rs | 1538 +++++++-------------- language_service/src/tests/test_fs.rs | 13 +- 5 files changed, 601 insertions(+), 1121 deletions(-) diff --git a/compiler/qsc_project/src/project.rs b/compiler/qsc_project/src/project.rs index 10d516aee2..1534bb18c3 100644 --- a/compiler/qsc_project/src/project.rs +++ b/compiler/qsc_project/src/project.rs @@ -3,7 +3,7 @@ use crate::{ manifest::{GitHubRef, PackageType}, - Manifest, ManifestDescriptor, PackageRef, + Manifest, PackageRef, }; use async_trait::async_trait; use futures::FutureExt; @@ -129,6 +129,11 @@ pub enum Error { #[error("Error fetching from GitHub: {0}")] #[diagnostic(code("Qsc.Project.GitHub"))] GitHub(String), + + #[error("File {0} is not listed in the `files` field of the manifest")] + #[help("To avoid unexpected behavior, add this file to the `files` field in the `qsharp.json` manifest")] + #[diagnostic(code("Qsc.Project.DocumentNotInProject"))] + DocumentNotInProject(String), } impl Error { @@ -137,6 +142,7 @@ impl Error { pub fn path(&self) -> Option<&String> { match self { Error::GitHubManifestParse { path, .. } + | Error::DocumentNotInProject(path) | Error::NoSrcDir { path } | Error::ManifestParse { path, .. } => Some(path), // Note we don't return the path for `FileSystem` errors, @@ -182,10 +188,7 @@ pub trait FileSystemAsync { ) -> miette::Result>; /// Given an initial path, fetch files matching /**/*.qs - async fn collect_project_sources( - &self, - initial_path: &Path, - ) -> ProjectResult> { + async fn collect_project_sources(&self, initial_path: &Path) -> ProjectResult> { let listing = self .list_directory(initial_path) .await @@ -210,7 +213,7 @@ pub trait FileSystemAsync { async fn collect_project_sources_inner( &self, initial_path: &Path, - ) -> ProjectResult> { + ) -> ProjectResult> { let listing = self .list_directory(initial_path) .await @@ -221,7 +224,7 @@ pub trait FileSystemAsync { let mut files = vec![]; for item in filter_hidden_files(listing.into_iter()) { match item.entry_type() { - Ok(EntryType::File) if item.entry_extension() == "qs" => files.push(item), + Ok(EntryType::File) if item.entry_extension() == "qs" => files.push(item.path()), Ok(EntryType::Folder) => { files.append(&mut self.collect_project_sources_inner(&item.path()).await?); } @@ -231,8 +234,64 @@ pub trait FileSystemAsync { Ok(files) } + async fn collect_sources_from_files_field( + &self, + project_path: &Path, + manifest: &Manifest, + ) -> ProjectResult> { + let mut v = vec![]; + for file in &manifest.files { + v.push( + self.resolve_path(project_path, Path::new(&file)) + .await + .map_err(|e| Error::FileSystem { + about_path: project_path.to_string_lossy().to_string(), + error: e.to_string(), + })?, + ); + } + Ok(v) + } + + fn validate( + &self, + qs_files: &mut Vec, + listed_files: &mut Vec, + ) -> Result<(), Vec> { + qs_files.sort(); + listed_files.sort(); + + // If the `files` field exists in the manifest, validate it includes + // all the files in the `src` directory. + // how do I subtract one sorted vector from another + let mut difference = qs_files.clone(); + let mut iter2 = listed_files.iter().peekable(); + + difference.retain(|item| { + while let Some(&next) = iter2.peek() { + if next < item { + iter2.next(); + } else { + break; + } + } + iter2.peek() != Some(&item) + }); + + if !difference.is_empty() { + return Err(difference + .iter() + .map(|p| Error::DocumentNotInProject(p.to_string_lossy().to_string())) + .collect()); + } + Ok(()) + } + /// Given a directory, loads the project sources /// and the sources for all its dependencies. + /// + /// Any errors that didn't block project load are contained in the + /// `errors` field of the returned `Project`. async fn load_project( &self, directory: &Path, @@ -243,15 +302,15 @@ pub trait FileSystemAsync { .await .map_err(|e| vec![e])?; - let root = self - .read_local_manifest_and_sources(directory) - .await - .map_err(|e| vec![e])?; - let mut errors = vec![]; let mut packages = FxHashMap::default(); let mut stack = vec![]; + let root = self + .read_local_manifest_and_sources(directory, &mut errors) + .await + .map_err(|e| vec![e])?; + let root_path = directory.to_string_lossy().to_string(); let root_ref = PackageRef::Path { path: root_path }; @@ -321,41 +380,33 @@ pub trait FileSystemAsync { /// Load the sources for a single package at the given directory. Also load its /// dependency information but don't recurse into dependencies yet. + /// + /// Any errors that didn't block project load are accumulated into the `errors` vector. async fn read_local_manifest_and_sources( &self, - directory: &Path, + manifest_dir: &Path, + errors: &mut Vec, ) -> ProjectResult { - let manifest = self.parse_manifest_in_dir(directory).await?; - - let manifest = ManifestDescriptor { - manifest_dir: directory.to_path_buf(), - manifest, - }; - - let project_path = manifest.manifest_dir.clone(); - - // If the `files` field exists in the manifest, prefer that. - // Otherwise, collect all files in the project directory. - let qs_files: Vec = if manifest.manifest.files.is_empty() { - let qs_files = self.collect_project_sources(&project_path).await?; - qs_files.into_iter().map(|file| file.path()).collect() - } else { - let mut v = vec![]; - for file in manifest.manifest.files { - v.push( - self.resolve_path(&project_path, Path::new(&file)) - .await - .map_err(|e| Error::FileSystem { - about_path: project_path.to_string_lossy().to_string(), - error: e.to_string(), - })?, - ); - } - v - }; + let manifest = self.parse_manifest_in_dir(manifest_dir).await?; + + // All the *.qs files under src/ + let mut all_qs_files = self.collect_project_sources(manifest_dir).await?; + + // Files explicitly listed in the `files` field of the manifest + let mut listed_files = self + .collect_sources_from_files_field(manifest_dir, &manifest) + .await?; + + if !listed_files.is_empty() { + errors.extend( + self.validate(&mut all_qs_files, &mut listed_files) + .err() + .unwrap_or_default(), + ); + } - let mut sources = Vec::with_capacity(qs_files.len()); - for path in qs_files { + let mut sources = Vec::with_capacity(all_qs_files.len()); + for path in all_qs_files { sources.push(self.read_file(&path).await.map_err(|e| Error::FileSystem { about_path: path.to_string_lossy().to_string(), error: e.to_string(), @@ -367,13 +418,13 @@ pub trait FileSystemAsync { // For any local dependencies, convert relative paths to absolute, // so that multiple references to the same package, from different packages, // get merged correctly. - for (alias, mut dep) in manifest.manifest.dependencies { + for (alias, mut dep) in manifest.dependencies { if let PackageRef::Path { path: dep_path } = &mut dep { *dep_path = self - .resolve_path(&project_path, &PathBuf::from(dep_path.clone())) + .resolve_path(manifest_dir, &PathBuf::from(dep_path.clone())) .await .map_err(|e| Error::FileSystem { - about_path: project_path.to_string_lossy().to_string(), + about_path: manifest_dir.to_string_lossy().to_string(), error: e.to_string(), })? .to_string_lossy() @@ -384,9 +435,9 @@ pub trait FileSystemAsync { Ok(PackageInfo { sources, - language_features: LanguageFeatures::from_iter(&manifest.manifest.language_features), + language_features: LanguageFeatures::from_iter(manifest.language_features), dependencies, - package_type: manifest.manifest.package_type, + package_type: manifest.package_type, }) } @@ -477,6 +528,7 @@ pub trait FileSystemAsync { global_cache: &RefCell, key: PackageKey, this_pkg: &PackageRef, + errors: &mut Vec, ) -> ProjectResult { match this_pkg { PackageRef::GitHub { github } => { @@ -499,7 +551,7 @@ pub trait FileSystemAsync { // editing experience as intuitive as possible. This may change if we start // hitting perf issues, but careful consideration is needed into when to // invalidate the cache. - self.read_local_manifest_and_sources(PathBuf::from(path.clone()).as_path()) + self.read_local_manifest_and_sources(PathBuf::from(path.clone()).as_path(), errors) .await } } @@ -535,7 +587,7 @@ pub trait FileSystemAsync { } let dep_result = self - .read_manifest_and_sources(global_cache, dep_key.clone(), &dependency) + .read_manifest_and_sources(global_cache, dep_key.clone(), &dependency, errors) .await; match dep_result { diff --git a/compiler/qsc_project/src/tests.rs b/compiler/qsc_project/src/tests.rs index a1af126fd5..529ed75624 100644 --- a/compiler/qsc_project/src/tests.rs +++ b/compiler/qsc_project/src/tests.rs @@ -426,6 +426,10 @@ fn explicit_files_list() { "explicit_files_list/src/Main.qs", "namespace Dependency {\n function LibraryFn() : Unit {\n }\n}\n", ), + ( + "explicit_files_list/src/NotIncluded.qs", + "namespace Dependency {\n function LibraryFn() : Unit {\n }\n}\n", + ), ], language_features: LanguageFeatures( 0, @@ -436,7 +440,11 @@ fn explicit_files_list() { packages: {}, }, lints: [], - errors: [], + errors: [ + DocumentNotInProject( + "explicit_files_list/src/NotIncluded.qs", + ), + ], }"#]], ); } diff --git a/compiler/qsc_project/src/tests/harness.rs b/compiler/qsc_project/src/tests/harness.rs index d1ff08962b..b6f0c4c008 100644 --- a/compiler/qsc_project/src/tests/harness.rs +++ b/compiler/qsc_project/src/tests/harness.rs @@ -59,7 +59,8 @@ fn normalize(project: &mut Project, root_path: &Path) { match err { Error::NoSrcDir { path } | Error::ManifestParse { path, .. } - | Error::GitHubManifestParse { path, .. } => { + | Error::GitHubManifestParse { path, .. } + | Error::DocumentNotInProject(path) => { let mut str = std::mem::take(path).into(); remove_absolute_path_prefix(&mut str, root_path); *path = str.to_string(); @@ -74,13 +75,11 @@ fn normalize(project: &mut Project, root_path: &Path) { *error = "REPLACED".to_string(); } Error::Circular(s1, s2) | Error::GitHubToLocal(s1, s2) => { - // These errors contain absolute paths which don't work well in test output + // these strings can contain mangled absolute paths so we can't fix them *s1 = "REPLACED".to_string(); *s2 = "REPLACED".to_string(); } - Error::GitHub(s) => { - *s = "REPLACED".to_string(); - } + Error::GitHub(_) => {} } } } diff --git a/language_service/src/state/tests.rs b/language_service/src/state/tests.rs index 2cf949e2c0..303955338d 100644 --- a/language_service/src/state/tests.rs +++ b/language_service/src/state/tests.rs @@ -6,13 +6,19 @@ use super::{CompilationState, CompilationStateUpdater}; use crate::{ - protocol::{DiagnosticUpdate, ErrorKind, NotebookMetadata, WorkspaceConfigurationUpdate}, + protocol::{DiagnosticUpdate, NotebookMetadata, WorkspaceConfigurationUpdate}, tests::test_fs::{dir, file, FsNode, TestProjectHost}, }; use expect_test::{expect, Expect}; -use qsc::{compile, project, target::Profile, LanguageFeatures, PackageType}; +use miette::Diagnostic; +use qsc::{target::Profile, LanguageFeatures, PackageType}; use qsc_linter::{AstLint, LintConfig, LintKind, LintLevel}; -use std::{cell::RefCell, fmt::Write, rc::Rc}; +use std::{ + cell::RefCell, + fmt::{Display, Write}, + rc::Rc, + str::from_utf8, +}; #[tokio::test] async fn no_error() { @@ -27,12 +33,7 @@ async fn no_error() { ) .await; - expect_errors( - &errors, - &expect![[r#" - [] - "#]], - ); + expect_errors(&errors, &expect!["[]"]); } #[tokio::test] @@ -48,35 +49,11 @@ async fn clear_error() { &errors, &expect![[r#" [ - ( - "single/foo.qs", - Some( - 1, - ), - [ - Frontend( - Error( - Parse( - Error( - Rule( - "identifier", - Open( - Brace, - ), - Span { - lo: 10, - hi: 11, - }, - ), - ), - ), - ), - ), - ], - [], - ), - ] - "#]], + uri: "single/foo.qs" version: Some(1) errors: [ + syntax error + [single/foo.qs] [{] + ], + ]"#]], ); updater @@ -91,16 +68,8 @@ async fn clear_error() { &errors, &expect![[r#" [ - ( - "single/foo.qs", - Some( - 2, - ), - [], - [], - ), - ] - "#]], + uri: "single/foo.qs" version: Some(2) errors: [], + ]"#]], ); } @@ -140,62 +109,20 @@ async fn close_last_doc_in_project() { } "#]], &expect![[r#" - project/qsharp.json: SourceMap { - sources: [ - Source { - name: "project/src/other_file.qs", - contents: "namespace Foo { @EntryPoint() operation Main() : Unit {} }", - offset: 0, - }, - Source { - name: "project/src/this_file.qs", - contents: "// DISK CONTENTS\n namespace Foo { }", - offset: 59, - }, - ], - common_prefix: Some( - "project/src/", - ), - entry: None, - } + project/qsharp.json: [ + "project/src/other_file.qs": "namespace Foo { @EntryPoint() operation Main() : Unit {} }", + "project/src/this_file.qs": "// DISK CONTENTS\n namespace Foo { }", + ], "#]], &expect![[r#" [ - ( - "project/src/this_file.qs", - Some( - 1, - ), - [ - Frontend( - Error( - Parse( - Error( - Token( - Eof, - ClosedBinOp( - Slash, - ), - Span { - lo: 59, - hi: 60, - }, - ), - ), - ), - ), - ), - ], - [], - ), - ( - "project/src/this_file.qs", - None, - [], - [], - ), - ] - "#]], + uri: "project/src/this_file.qs" version: Some(1) errors: [ + syntax error + [project/src/this_file.qs] [/] + ], + + uri: "project/src/this_file.qs" version: None errors: [], + ]"#]], ); updater.close_document("project/src/other_file.qs").await; @@ -207,9 +134,7 @@ async fn close_last_doc_in_project() { {} "#]], &expect![""], - &expect![[r#" - [] - "#]], + &expect!["[]"], ); } @@ -226,35 +151,11 @@ async fn clear_on_document_close() { &errors, &expect![[r#" [ - ( - "single/foo.qs", - Some( - 1, - ), - [ - Frontend( - Error( - Parse( - Error( - Rule( - "identifier", - Open( - Brace, - ), - Span { - lo: 10, - hi: 11, - }, - ), - ), - ), - ), - ), - ], - [], - ), - ] - "#]], + uri: "single/foo.qs" version: Some(1) errors: [ + syntax error + [single/foo.qs] [{] + ], + ]"#]], ); updater.close_document("single/foo.qs").await; @@ -263,14 +164,8 @@ async fn clear_on_document_close() { &errors, &expect![[r#" [ - ( - "single/foo.qs", - None, - [], - [], - ), - ] - "#]], + uri: "single/foo.qs" version: None errors: [], + ]"#]], ); } @@ -287,33 +182,11 @@ async fn compile_error() { &errors, &expect![[r#" [ - ( - "single/foo.qs", - Some( - 1, - ), - [ - Frontend( - Error( - Parse( - Error( - Token( - Eof, - Ident, - Span { - lo: 0, - hi: 9, - }, - ), - ), - ), - ), - ), - ], - [], - ), - ] - "#]], + uri: "single/foo.qs" version: Some(1) errors: [ + syntax error + [single/foo.qs] [badsyntax] + ], + ]"#]], ); } @@ -337,37 +210,13 @@ async fn rca_errors_are_reported_when_compilation_succeeds() { &errors, &expect![[r#" [ - ( - "single/foo.qs", - Some( - 1, - ), - [ - Pass( - CapabilitiesCk( - UseOfDynamicDouble( - Span { - lo: 106, - hi: 117, - }, - ), - ), - ), - Pass( - CapabilitiesCk( - UseOfDynamicDouble( - Span { - lo: 121, - hi: 122, - }, - ), - ), - ), - ], - [], - ), - ] - "#]], + uri: "single/foo.qs" version: Some(1) errors: [ + cannot use a dynamic double value + [single/foo.qs] [set x = 2.0] + cannot use a dynamic double value + [single/foo.qs] [x] + ], + ]"#]], ); } @@ -391,47 +240,15 @@ async fn base_profile_rca_errors_are_reported_when_compilation_succeeds() { &errors, &expect![[r#" [ - ( - "single/foo.qs", - Some( - 1, - ), - [ - Pass( - CapabilitiesCk( - UseOfDynamicBool( - Span { - lo: 86, - hi: 103, - }, - ), - ), - ), - Pass( - CapabilitiesCk( - UseOfDynamicDouble( - Span { - lo: 106, - hi: 117, - }, - ), - ), - ), - Pass( - CapabilitiesCk( - UseOfDynamicDouble( - Span { - lo: 121, - hi: 122, - }, - ), - ), - ), - ], - [], - ), - ] - "#]], + uri: "single/foo.qs" version: Some(1) errors: [ + cannot use a dynamic bool value + [single/foo.qs] [MResetZ(q) == One] + cannot use a dynamic double value + [single/foo.qs] [set x = 2.0] + cannot use a dynamic double value + [single/foo.qs] [x] + ], + ]"#]], ); } @@ -453,12 +270,7 @@ async fn package_type_update_causes_error() { ) .await; - expect_errors( - &errors, - &expect![[r#" - [] - "#]], - ); + expect_errors(&errors, &expect!["[]"]); updater.update_configuration(WorkspaceConfigurationUpdate { package_type: Some(PackageType::Exe), @@ -469,22 +281,10 @@ async fn package_type_update_causes_error() { &errors, &expect![[r#" [ - ( - "single/foo.qs", - Some( - 1, - ), - [ - Pass( - EntryPoint( - NotFound, - ), - ), - ], - [], - ), - ] - "#]], + uri: "single/foo.qs" version: Some(1) errors: [ + entry point not found + ], + ]"#]], ); } @@ -511,27 +311,11 @@ async fn target_profile_update_fixes_error() { &errors, &expect![[r#" [ - ( - "single/foo.qs", - Some( - 1, - ), - [ - Pass( - CapabilitiesCk( - UseOfDynamicBool( - Span { - lo: 62, - hi: 74, - }, - ), - ), - ), - ], - [], - ), - ] - "#]], + uri: "single/foo.qs" version: Some(1) errors: [ + cannot use a dynamic bool value + [single/foo.qs] [M(q) == Zero] + ], + ]"#]], ); updater.update_configuration(WorkspaceConfigurationUpdate { @@ -543,16 +327,8 @@ async fn target_profile_update_fixes_error() { &errors, &expect![[r#" [ - ( - "single/foo.qs", - Some( - 1, - ), - [], - [], - ), - ] - "#]], + uri: "single/foo.qs" version: Some(1) errors: [], + ]"#]], ); } @@ -567,12 +343,7 @@ async fn target_profile_update_causes_error_in_stdlib() { r#"namespace Foo { @EntryPoint() operation Main() : Unit { use q = Qubit(); let r = M(q); let b = Microsoft.Quantum.Convert.ResultAsBool(r); } }"#, ).await; - expect_errors( - &errors, - &expect![[r#" - [] - "#]], - ); + expect_errors(&errors, &expect!["[]"]); updater.update_configuration(WorkspaceConfigurationUpdate { target_profile: Some(Profile::Base), @@ -583,27 +354,11 @@ async fn target_profile_update_causes_error_in_stdlib() { &errors, &expect![[r#" [ - ( - "single/foo.qs", - Some( - 1, - ), - [ - Pass( - CapabilitiesCk( - UseOfDynamicBool( - Span { - lo: 95, - hi: 136, - }, - ), - ), - ), - ], - [], - ), - ] - "#]], + uri: "single/foo.qs" version: Some(1) errors: [ + cannot use a dynamic bool value + [single/foo.qs] [Microsoft.Quantum.Convert.ResultAsBool(r)] + ], + ]"#]], ); } @@ -624,12 +379,7 @@ async fn notebook_document_no_errors() { ) .await; - expect_errors( - &errors, - &expect![[r#" - [] - "#]], - ); + expect_errors(&errors, &expect!["[]"]); } #[tokio::test] @@ -653,44 +403,13 @@ async fn notebook_document_errors() { &errors, &expect![[r#" [ - ( - "cell2", - Some( - 1, - ), - [ - Frontend( - Error( - Resolve( - NotFound( - "Foo", - Span { - lo: 27, - hi: 30, - }, - ), - ), - ), - ), - Frontend( - Error( - Type( - Error( - AmbiguousTy( - Span { - lo: 27, - hi: 32, - }, - ), - ), - ), - ), - ), - ], - [], - ), - ] - "#]], + uri: "cell2" version: Some(1) errors: [ + name error + [cell2] [Foo] + type error + [cell2] [Foo()] + ], + ]"#]], ); } @@ -715,64 +434,16 @@ async fn notebook_document_lints() { &errors, &expect![[r#" [ - ( - "cell1", - Some( - 1, - ), - [ - Lint( - Lint { - span: Span { - lo: 34, - hi: 37, - }, - level: Warn, - message: "redundant semicolons", - help: "remove the redundant semicolons", - kind: Ast( - RedundantSemicolons, - ), - code_action_edits: [ - ( - "", - Span { - lo: 34, - hi: 37, - }, - ), - ], - }, - ), - ], - [], - ), - ( - "cell2", - Some( - 1, - ), - [ - Lint( - Lint { - span: Span { - lo: 72, - hi: 77, - }, - level: Error, - message: "attempt to divide by zero", - help: "division by zero will fail at runtime", - kind: Ast( - DivisionByZero, - ), - code_action_edits: [], - }, - ), - ], - [], - ), - ] - "#]], + uri: "cell1" version: Some(1) errors: [ + redundant semicolons + [cell1] [;;;] + ], + + uri: "cell2" version: Some(1) errors: [ + attempt to divide by zero + [cell2] [5 / 0] + ], + ]"#]], ); } @@ -797,44 +468,13 @@ async fn notebook_update_remove_cell_clears_errors() { &errors, &expect![[r#" [ - ( - "cell2", - Some( - 1, - ), - [ - Frontend( - Error( - Resolve( - NotFound( - "Foo", - Span { - lo: 27, - hi: 30, - }, - ), - ), - ), - ), - Frontend( - Error( - Type( - Error( - AmbiguousTy( - Span { - lo: 27, - hi: 32, - }, - ), - ), - ), - ), - ), - ], - [], - ), - ] - "#]], + uri: "cell2" version: Some(1) errors: [ + name error + [cell2] [Foo] + type error + [cell2] [Foo()] + ], + ]"#]], ); updater @@ -849,14 +489,8 @@ async fn notebook_update_remove_cell_clears_errors() { &errors, &expect![[r#" [ - ( - "cell2", - None, - [], - [], - ), - ] - "#]], + uri: "cell2" version: None errors: [], + ]"#]], ); } @@ -881,44 +515,13 @@ async fn close_notebook_clears_errors() { &errors, &expect![[r#" [ - ( - "cell2", - Some( - 1, - ), - [ - Frontend( - Error( - Resolve( - NotFound( - "Foo", - Span { - lo: 27, - hi: 30, - }, - ), - ), - ), - ), - Frontend( - Error( - Type( - Error( - AmbiguousTy( - Span { - lo: 27, - hi: 32, - }, - ), - ), - ), - ), - ), - ], - [], - ), - ] - "#]], + uri: "cell2" version: Some(1) errors: [ + name error + [cell2] [Foo] + type error + [cell2] [Foo()] + ], + ]"#]], ); updater.close_notebook_document("notebook.ipynb"); @@ -927,14 +530,8 @@ async fn close_notebook_clears_errors() { &errors, &expect![[r#" [ - ( - "cell2", - None, - [], - [], - ), - ] - "#]], + uri: "cell2" version: None errors: [], + ]"#]], ); } @@ -975,12 +572,7 @@ async fn update_notebook_with_valid_dependencies() { ) .await; - expect_errors( - &errors, - &expect![[r#" - [] - "#]], - ); + expect_errors(&errors, &expect!["[]"]); } #[tokio::test] @@ -1024,93 +616,22 @@ async fn update_notebook_reports_errors_from_dependencies() { &errors, &expect![[r#" [ - ( - "cell1", - Some( - 1, - ), - [ - Frontend( - Error( - Resolve( - NotFound( - "Foo", - Span { - lo: 5, - hi: 8, - }, - ), - ), - ), - ), - Frontend( - Error( - Resolve( - NotFound( - "Foo", - Span { - lo: 5, - hi: 8, - }, - ), - ), - ), - ), - Frontend( - Error( - Resolve( - NotFound( - "Bar", - Span { - lo: 9, - hi: 12, - }, - ), - ), - ), - ), - Frontend( - Error( - Type( - Error( - AmbiguousTy( - Span { - lo: 9, - hi: 14, - }, - ), - ), - ), - ), - ), - ], - [], - ), - ( - "project/src/file.qs", - None, - [ - Frontend( - Error( - Type( - Error( - TyMismatch( - "Unit", - "Int", - Span { - lo: 33, - hi: 36, - }, - ), - ), - ), - ), - ), - ], - [], - ), - ] - "#]], + uri: "cell1" version: Some(1) errors: [ + name error + [cell1] [Foo] + name error + [cell1] [Foo] + name error + [cell1] [Bar] + type error + [cell1] [Bar()] + ], + + uri: "project/src/file.qs" version: None errors: [ + type error + [project/src/file.qs] [Int] + ], + ]"#]], ); } @@ -1172,32 +693,12 @@ async fn update_notebook_reports_errors_from_dependency_of_dependencies() { expect_errors( &errors, &expect![[r#" - [ - ( - "project2/src/file.qs", - None, - [ - Frontend( - Error( - Type( - Error( - TyMismatch( - "Unit", - "Int", - Span { - lo: 33, - hi: 36, - }, - ), - ), - ), - ), - ), - ], - [], - ), - ] - "#]], + [ + uri: "project2/src/file.qs" version: None errors: [ + type error + [project2/src/file.qs] [Int] + ], + ]"#]], ); } @@ -1239,56 +740,218 @@ async fn update_doc_updates_project() { } "#]], &expect![[r#" - project/qsharp.json: SourceMap { - sources: [ - Source { - name: "project/src/other_file.qs", - contents: "namespace Foo { @EntryPoint() operation Main() : Unit {} }", - offset: 0, - }, - Source { - name: "project/src/this_file.qs", - contents: "namespace Foo { we should see this in the source }", - offset: 59, - }, - ], - common_prefix: Some( - "project/src/", - ), - entry: None, - } + project/qsharp.json: [ + "project/src/other_file.qs": "namespace Foo { @EntryPoint() operation Main() : Unit {} }", + "project/src/this_file.qs": "namespace Foo { we should see this in the source }", + ], "#]], &expect![[r#" [ - ( - "project/src/this_file.qs", - Some( - 1, - ), + uri: "project/src/this_file.qs" version: Some(1) errors: [ + syntax error + [project/src/this_file.qs] [we] + ], + ]"#]], + ); +} + +#[tokio::test] +async fn file_not_in_files_list() { + let received_errors = RefCell::new(Vec::new()); + + // Manifest has a "files" field. + // One file is listed in it, the other is not. + // This shouldn't block project load, but should generate an error. + let fs = FsNode::Dir( + [dir( + "project", + [ + file( + "qsharp.json", + r#"{ + "files" : [ + "src/explicitly_listed.qs" + ] + }"#, + ), + dir( + "src", [ - Frontend( - Error( - Parse( - Error( - Token( - Close( - Brace, - ), - Ident, - Span { - lo: 75, - hi: 77, - }, - ), - ), - ), - ), - ), + file("explicitly_listed.qs", "// CONTENTS"), + file("unlisted.qs", "// CONTENTS"), ], - [], ), - ] + ], + )] + .into_iter() + .collect(), + ); + + let fs = Rc::new(RefCell::new(fs)); + let mut updater = new_updater_with_file_system(&received_errors, &fs); + + // Open the file that is listed in the files list + updater + .update_document("project/src/explicitly_listed.qs", 1, "// CONTENTS") + .await; + + // The whole project should be loaded, which should generate + // an error about the other file that's unlisted. + // They are both in the compilation. + check_state_and_errors( + &updater, + &received_errors, + &expect![[r#" + { + "project/src/explicitly_listed.qs": OpenDocument { + version: 1, + compilation: "project/qsharp.json", + latest_str_content: "// CONTENTS", + }, + } + "#]], + &expect![[r#" + project/qsharp.json: [ + "project/src/explicitly_listed.qs": "// CONTENTS", + "project/src/unlisted.qs": "// CONTENTS", + ], + "#]], + &expect![[r#" + [ + uri: "project/src/unlisted.qs" version: None errors: [ + File project/src/unlisted.qs is not listed in the `files` field of the manifest + ], + ]"#]], + ); + + // Open the unlisted file as well. + updater + .update_document("project/src/unlisted.qs", 1, "// CONTENTS") + .await; + + // Documents are both open and correctly associated with the project. + // The error about the unlisted file persists. + check_state_and_errors( + &updater, + &received_errors, + &expect![[r#" + { + "project/src/explicitly_listed.qs": OpenDocument { + version: 1, + compilation: "project/qsharp.json", + latest_str_content: "// CONTENTS", + }, + "project/src/unlisted.qs": OpenDocument { + version: 1, + compilation: "project/qsharp.json", + latest_str_content: "// CONTENTS", + }, + } + "#]], + &expect![[r#" + project/qsharp.json: [ + "project/src/explicitly_listed.qs": "// CONTENTS", + "project/src/unlisted.qs": "// CONTENTS", + ], "#]], + &expect![[r#" + [ + uri: "project/src/unlisted.qs" version: Some(1) errors: [ + File project/src/unlisted.qs is not listed in the `files` field of the manifest + ], + ]"#]], + ); +} + +#[tokio::test] +async fn file_not_under_src() { + let received_errors = RefCell::new(Vec::new()); + + // One file lives under the 'src' directory, the other does not. + // The one that isn't under 'src' should not be associated with the project. + let fs = FsNode::Dir( + [dir( + "project", + [ + file( + "qsharp.json", + r#"{ + "files" : [ + "src/under_src.qs" + ] + }"#, + ), + file("not_under_src.qs", "// CONTENTS"), + dir("src", [file("under_src.qs", "// CONTENTS")]), + ], + )] + .into_iter() + .collect(), + ); + + let fs = Rc::new(RefCell::new(fs)); + let mut updater = new_updater_with_file_system(&received_errors, &fs); + + // Open the file that is not under src. + updater + .update_document("project/not_under_src.qs", 1, "// CONTENTS") + .await; + + // This document is not associated with the manifest, + // didn't cause the manifest to be loaded, + // and lives in its own project by itself. + check_state_and_errors( + &updater, + &received_errors, + &expect![[r#" + { + "project/not_under_src.qs": OpenDocument { + version: 1, + compilation: "project/not_under_src.qs", + latest_str_content: "// CONTENTS", + }, + } + "#]], + &expect![[r#" + project/not_under_src.qs: [ + "project/not_under_src.qs": "// CONTENTS", + ], + "#]], + &expect!["[]"], + ); + + // Open the file that's properly under the "src" directory. + updater + .update_document("project/src/under_src.qs", 1, "// CONTENTS") + .await; + + // The manifest is loaded, `not_under_src.qs` is still not associated with it. + check_state_and_errors( + &updater, + &received_errors, + &expect![[r#" + { + "project/not_under_src.qs": OpenDocument { + version: 1, + compilation: "project/not_under_src.qs", + latest_str_content: "// CONTENTS", + }, + "project/src/under_src.qs": OpenDocument { + version: 1, + compilation: "project/qsharp.json", + latest_str_content: "// CONTENTS", + }, + } + "#]], + &expect![[r#" + project/not_under_src.qs: [ + "project/not_under_src.qs": "// CONTENTS", + ], + project/qsharp.json: [ + "project/src/under_src.qs": "// CONTENTS", + ], + "#]], + &expect!["[]"], ); } @@ -1333,62 +996,20 @@ async fn close_doc_prioritizes_fs() { } "#]], &expect![[r#" - project/qsharp.json: SourceMap { - sources: [ - Source { - name: "project/src/other_file.qs", - contents: "namespace Foo { @EntryPoint() operation Main() : Unit {} }", - offset: 0, - }, - Source { - name: "project/src/this_file.qs", - contents: "// DISK CONTENTS\n namespace Foo { }", - offset: 59, - }, - ], - common_prefix: Some( - "project/src/", - ), - entry: None, - } + project/qsharp.json: [ + "project/src/other_file.qs": "namespace Foo { @EntryPoint() operation Main() : Unit {} }", + "project/src/this_file.qs": "// DISK CONTENTS\n namespace Foo { }", + ], "#]], &expect![[r#" [ - ( - "project/src/this_file.qs", - Some( - 1, - ), - [ - Frontend( - Error( - Parse( - Error( - Token( - Eof, - ClosedBinOp( - Slash, - ), - Span { - lo: 59, - hi: 60, - }, - ), - ), - ), - ), - ), - ], - [], - ), - ( - "project/src/this_file.qs", - None, - [], - [], - ), - ] - "#]], + uri: "project/src/this_file.qs" version: Some(1) errors: [ + syntax error + [project/src/this_file.qs] [/] + ], + + uri: "project/src/this_file.qs" version: None errors: [], + ]"#]], ); } @@ -1417,24 +1038,10 @@ async fn delete_manifest() { } "#]], &expect![[r#" - project/qsharp.json: SourceMap { - sources: [ - Source { - name: "project/src/other_file.qs", - contents: "// DISK CONTENTS\n namespace OtherFile { operation Other() : Unit { } }", - offset: 0, - }, - Source { - name: "project/src/this_file.qs", - contents: "// DISK CONTENTS\n namespace Foo { }", - offset: 71, - }, - ], - common_prefix: Some( - "project/src/", - ), - entry: None, - } + project/qsharp.json: [ + "project/src/other_file.qs": "// DISK CONTENTS\n namespace OtherFile { operation Other() : Unit { } }", + "project/src/this_file.qs": "// DISK CONTENTS\n namespace Foo { }", + ], "#]], ); @@ -1460,19 +1067,9 @@ async fn delete_manifest() { } "#]], &expect![[r#" - project/src/this_file.qs: SourceMap { - sources: [ - Source { - name: "project/src/this_file.qs", - contents: "// DISK CONTENTS\n namespace Foo { }", - offset: 0, - }, - ], - common_prefix: Some( - "project/src/", - ), - entry: None, - } + project/src/this_file.qs: [ + "project/src/this_file.qs": "// DISK CONTENTS\n namespace Foo { }", + ], "#]], ); } @@ -1502,24 +1099,10 @@ async fn delete_manifest_then_close() { } "#]], &expect![[r#" - project/qsharp.json: SourceMap { - sources: [ - Source { - name: "project/src/other_file.qs", - contents: "// DISK CONTENTS\n namespace OtherFile { operation Other() : Unit { } }", - offset: 0, - }, - Source { - name: "project/src/this_file.qs", - contents: "// DISK CONTENTS\n namespace Foo { }", - offset: 71, - }, - ], - common_prefix: Some( - "project/src/", - ), - entry: None, - } + project/qsharp.json: [ + "project/src/other_file.qs": "// DISK CONTENTS\n namespace OtherFile { operation Other() : Unit { } }", + "project/src/this_file.qs": "// DISK CONTENTS\n namespace Foo { }", + ], "#]], ); @@ -1566,24 +1149,10 @@ async fn doc_switches_project() { } "#]], &expect![[r#" - nested_projects/src/subdir/qsharp.json: SourceMap { - sources: [ - Source { - name: "nested_projects/src/subdir/src/a.qs", - contents: "namespace A {}", - offset: 0, - }, - Source { - name: "nested_projects/src/subdir/src/b.qs", - contents: "namespace B {}", - offset: 15, - }, - ], - common_prefix: Some( - "nested_projects/src/subdir/src/", - ), - entry: None, - } + nested_projects/src/subdir/qsharp.json: [ + "nested_projects/src/subdir/src/a.qs": "namespace A {}", + "nested_projects/src/subdir/src/b.qs": "namespace B {}", + ], "#]], ); @@ -1622,24 +1191,10 @@ async fn doc_switches_project() { } "#]], &expect![[r#" - nested_projects/qsharp.json: SourceMap { - sources: [ - Source { - name: "nested_projects/src/subdir/src/a.qs", - contents: "namespace A {}", - offset: 0, - }, - Source { - name: "nested_projects/src/subdir/src/b.qs", - contents: "namespace B {}", - offset: 15, - }, - ], - common_prefix: Some( - "nested_projects/src/subdir/src/", - ), - entry: None, - } + nested_projects/qsharp.json: [ + "nested_projects/src/subdir/src/a.qs": "namespace A {}", + "nested_projects/src/subdir/src/b.qs": "namespace B {}", + ], "#]], ); } @@ -1674,24 +1229,10 @@ async fn doc_switches_project_on_close() { } "#]], &expect![[r#" - nested_projects/src/subdir/qsharp.json: SourceMap { - sources: [ - Source { - name: "nested_projects/src/subdir/src/a.qs", - contents: "namespace A {}", - offset: 0, - }, - Source { - name: "nested_projects/src/subdir/src/b.qs", - contents: "namespace B {}", - offset: 15, - }, - ], - common_prefix: Some( - "nested_projects/src/subdir/src/", - ), - entry: None, - } + nested_projects/src/subdir/qsharp.json: [ + "nested_projects/src/subdir/src/a.qs": "namespace A {}", + "nested_projects/src/subdir/src/b.qs": "namespace B {}", + ], "#]], ); @@ -1723,24 +1264,10 @@ async fn doc_switches_project_on_close() { } "#]], &expect![[r#" - nested_projects/qsharp.json: SourceMap { - sources: [ - Source { - name: "nested_projects/src/subdir/src/a.qs", - contents: "namespace A {}", - offset: 0, - }, - Source { - name: "nested_projects/src/subdir/src/b.qs", - contents: "namespace B {}", - offset: 15, - }, - ], - common_prefix: Some( - "nested_projects/src/subdir/src/", - ), - entry: None, - } + nested_projects/qsharp.json: [ + "nested_projects/src/subdir/src/a.qs": "namespace A {}", + "nested_projects/src/subdir/src/b.qs": "namespace B {}", + ], "#]], ); } @@ -1831,57 +1358,17 @@ async fn lints_update_after_manifest_change() { .await; // Check generated lints. - let lints: &[compile::ErrorKind] = &received_errors.take()[0].2; - check_lints( - lints, + expect_errors( + &received_errors, &expect![[r#" - [ - Lint( - Lint { - span: Span { - lo: 71, - hi: 78, - }, - level: Error, - message: "unnecessary parentheses", - help: "remove the extra parentheses for clarity", - kind: Ast( - NeedlessParens, - ), - code_action_edits: [ - ( - "", - Span { - lo: 71, - hi: 72, - }, - ), - ( - "", - Span { - lo: 77, - hi: 78, - }, - ), - ], - }, - ), - Lint( - Lint { - span: Span { - lo: 63, - hi: 68, - }, - level: Error, - message: "attempt to divide by zero", - help: "division by zero will fail at runtime", - kind: Ast( - DivisionByZero, - ), - code_action_edits: [], - }, - ), - ]"#]], + [ + uri: "project/src/this_file.qs" version: Some(1) errors: [ + unnecessary parentheses + [project/src/this_file.qs] [(2 ^ 4)] + attempt to divide by zero + [project/src/this_file.qs] [5 / 0] + ], + ]"#]], ); // Modify the manifest. @@ -1896,56 +1383,16 @@ async fn lints_update_after_manifest_change() { .await; // Check lints again - let lints: &[compile::ErrorKind] = &received_errors.take()[0].2; - check_lints( - lints, + expect_errors( + &received_errors, &expect![[r#" [ - Lint( - Lint { - span: Span { - lo: 71, - hi: 78, - }, - level: Warn, - message: "unnecessary parentheses", - help: "remove the extra parentheses for clarity", - kind: Ast( - NeedlessParens, - ), - code_action_edits: [ - ( - "", - Span { - lo: 71, - hi: 72, - }, - ), - ( - "", - Span { - lo: 77, - hi: 78, - }, - ), - ], - }, - ), - Lint( - Lint { - span: Span { - lo: 63, - hi: 68, - }, - level: Warn, - message: "attempt to divide by zero", - help: "division by zero will fail at runtime", - kind: Ast( - DivisionByZero, - ), - code_action_edits: [], - }, - ), + uri: "project/src/this_file.qs" version: Some(1) errors: [ + unnecessary parentheses + [project/src/this_file.qs] [(2 ^ 4)] + attempt to divide by zero + [project/src/this_file.qs] [5 / 0] + ], ]"#]], ); } @@ -1971,26 +1418,14 @@ async fn lints_prefer_workspace_over_defaults() { .await; // Check generated lints. - let lints: &[compile::ErrorKind] = &received_errors.take()[0].2; - check_lints( - lints, + expect_errors( + &received_errors, &expect![[r#" [ - Lint( - Lint { - span: Span { - lo: 134, - hi: 139, - }, - level: Warn, - message: "attempt to divide by zero", - help: "division by zero will fail at runtime", - kind: Ast( - DivisionByZero, - ), - code_action_edits: [], - }, - ), + uri: "project/src/this_file.qs" version: Some(1) errors: [ + attempt to divide by zero + [project/src/this_file.qs] [5 / 0] + ], ]"#]], ); } @@ -2060,22 +1495,15 @@ async fn missing_dependency_reported() { .update_document("parent/src/main.qs", 1, "function Main() : Unit {}") .await; - expect![[r#" - [ - ( - "parent/qsharp.json", - None, - [], - [ - FileSystem { - about_path: "child/qsharp.json", - error: "file not found", - }, - ], - ), - ] - "#]] - .assert_debug_eq(&received_errors.borrow()); + expect_errors( + &received_errors, + &expect![[r#" + [ + uri: "parent/qsharp.json" version: None errors: [ + File system error: child/qsharp.json: file not found + ], + ]"#]], + ); } #[tokio::test] @@ -2113,62 +1541,59 @@ async fn error_from_dependency_reported() { .update_document("parent/src/main.qs", 1, "function Main() : Unit {}") .await; - expect![[r#" - [ - ( - "child/src/main.qs", - None, - [ - Frontend( - Error( - Parse( - Error( - Token( - Eof, - Ident, - Span { - lo: 0, - hi: 13, - }, - ), - ), - ), - ), - ), - ], - [], - ), - ] - "#]] - .assert_debug_eq(&received_errors.borrow()); + expect_errors( + &received_errors, + &expect![[r#" + [ + uri: "child/src/main.qs" version: None errors: [ + syntax error + [child/src/main.qs] [broken_syntax] + ], + ]"#]], + ); } -type ErrorInfo = ( - String, - Option, - Vec, - Vec, -); +impl Display for DiagnosticUpdate { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let DiagnosticUpdate { + uri, + version, + errors, + } = self; + + write!(f, "uri: {uri:?} version: {version:?} errors: [",)?; + // Formatting loosely taken from compiler/qsc/src/interpret/tests.rs + for error in errors { + write!(f, "\n {error}")?; + for label in error.labels().into_iter().flatten() { + let span = error + .source_code() + .expect("expected valid source code") + .read_span(label.inner(), 0, 0) + .expect("expected to be able to read span"); + + write!( + f, + "\n {} [{}] [{}]", + label.label().unwrap_or(""), + span.name().expect("expected source file name"), + from_utf8(span.data()).expect("expected valid utf-8 string"), + )?; + } + } + if !errors.is_empty() { + write!(f, "\n ")?; + } + writeln!(f, "],")?; + + Ok(()) + } +} -fn new_updater(received_errors: &RefCell>) -> CompilationStateUpdater<'_> { +fn new_updater(received_errors: &RefCell>) -> CompilationStateUpdater<'_> { let diagnostic_receiver = move |update: DiagnosticUpdate| { - let project_errors = update.errors.iter().filter_map(|error| match error { - ErrorKind::Project(error) => Some(error.clone()), - ErrorKind::Compile(_) => None, - }); - let compile_errors = update.errors.iter().filter_map(|error| match error { - ErrorKind::Compile(error) => Some(error.error().clone()), - ErrorKind::Project(_) => None, - }); - let mut v = received_errors.borrow_mut(); - - v.push(( - update.uri, - update.version, - compile_errors.collect(), - project_errors.collect(), - )); + v.push(update); }; CompilationStateUpdater::new( @@ -2181,27 +1606,12 @@ fn new_updater(received_errors: &RefCell>) -> CompilationStateUpd } fn new_updater_with_file_system<'a>( - received_errors: &'a RefCell>, + received_errors: &'a RefCell>, fs: &Rc>, ) -> CompilationStateUpdater<'a> { let diagnostic_receiver = move |update: DiagnosticUpdate| { - let project_errors = update.errors.iter().filter_map(|error| match error { - ErrorKind::Project(error) => Some(error.clone()), - ErrorKind::Compile(_) => None, - }); - let compile_errors = update.errors.iter().filter_map(|error| match error { - ErrorKind::Compile(error) => Some(error.error().clone()), - ErrorKind::Project(_) => None, - }); - let mut v = received_errors.borrow_mut(); - - v.push(( - update.uri, - update.version, - compile_errors.collect(), - project_errors.collect(), - )); + v.push(update); }; CompilationStateUpdater::new( @@ -2211,10 +1621,18 @@ fn new_updater_with_file_system<'a>( ) } -fn expect_errors(errors: &RefCell>, expected: &Expect) { - expected.assert_debug_eq(&errors.borrow()); +fn expect_errors(updates: &RefCell>, expected: &Expect) { + let mut buf = String::new(); + let _ = buf.write_str("["); + for update in updates.borrow().iter() { + let _ = write!(buf, "\n {update}"); + } + let _ = buf.write_str("]"); + + expected.assert_eq(&buf); + // reset accumulated errors after each check - errors.borrow_mut().clear(); + updates.borrow_mut().clear(); } fn assert_compilation_sources(updater: &CompilationStateUpdater<'_>, expected: &Expect) { @@ -2225,7 +1643,11 @@ fn assert_compilation_sources(updater: &CompilationStateUpdater<'_>, expected: & .compilations .iter() .fold(String::new(), |mut output, (name, compilation)| { - let _ = writeln!(output, "{}: {:#?}", name, compilation.0.user_unit().sources); + let _ = writeln!(output, "{name}: ["); + for source in compilation.0.user_unit().sources.iter() { + let _ = writeln!(output, " {:?}: {:?},", source.name, source.contents); + } + let _ = writeln!(output, "],"); output }); expected.assert_eq(&compilation_sources); @@ -2238,14 +1660,14 @@ fn assert_open_documents(updater: &CompilationStateUpdater<'_>, expected: &Expec fn check_state_and_errors( updater: &CompilationStateUpdater<'_>, - received_errors: &RefCell>, + received_diag_updates: &RefCell>, expected_open_documents: &Expect, expected_compilation_sources: &Expect, expected_errors: &Expect, ) { assert_open_documents(updater, expected_open_documents); assert_compilation_sources(updater, expected_compilation_sources); - expect_errors(received_errors, expected_errors); + expect_errors(received_diag_updates, expected_errors); } fn check_state( @@ -2270,10 +1692,6 @@ async fn check_lints_config(updater: &CompilationStateUpdater<'_>, expected_conf expected_config.assert_eq(&format!("{lints_config:#?}")); } -fn check_lints(lints: &[compile::ErrorKind], expected_lints: &Expect) { - expected_lints.assert_eq(&format!("{lints:#?}")); -} - thread_local! { static TEST_FS: Rc> = Rc::new(RefCell::new(test_fs()))} fn test_fs() -> FsNode { diff --git a/language_service/src/tests/test_fs.rs b/language_service/src/tests/test_fs.rs index 4af49e58a6..bbfa106bcb 100644 --- a/language_service/src/tests/test_fs.rs +++ b/language_service/src/tests/test_fs.rs @@ -103,14 +103,17 @@ impl FsNode { let mut curr = Some(self); let mut curr_path = String::new(); let mut last_manifest_dir = None; - let mut last_manifest = None; + // Traverse the path, keeping track of the last directory containing a + // manifest file. If we find a directory containing a manifest file and + // the next part of the path is "src", that's a candidate. for part in file.split('/') { - curr = curr.and_then(|node| match node { + curr = curr.and_then(|curr| match curr { FsNode::Dir(dir) => { - if let Some(FsNode::File(manifest)) = dir.get("qsharp.json") { - last_manifest_dir = Some(curr_path.trim_end_matches('/').to_string()); - last_manifest = Some(manifest); + if let Some(FsNode::File(_)) = dir.get("qsharp.json") { + if part == "src" { + last_manifest_dir = Some(curr_path.trim_end_matches('/').to_string()); + } } curr_path = format!("{curr_path}{part}/"); dir.get(part)