Skip to content

Commit a3d9843

Browse files
committed
fix language service panic when file is under the project folder but not in the files list
1 parent ca1bc18 commit a3d9843

File tree

5 files changed

+601
-1121
lines changed

5 files changed

+601
-1121
lines changed

compiler/qsc_project/src/project.rs

Lines changed: 102 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
use crate::{
55
manifest::{GitHubRef, PackageType},
6-
Manifest, ManifestDescriptor, PackageRef,
6+
Manifest, PackageRef,
77
};
88
use async_trait::async_trait;
99
use futures::FutureExt;
@@ -129,6 +129,11 @@ pub enum Error {
129129
#[error("Error fetching from GitHub: {0}")]
130130
#[diagnostic(code("Qsc.Project.GitHub"))]
131131
GitHub(String),
132+
133+
#[error("File {0} is not listed in the `files` field of the manifest")]
134+
#[help("To avoid unexpected behavior, add this file to the `files` field in the `qsharp.json` manifest")]
135+
#[diagnostic(code("Qsc.Project.DocumentNotInProject"))]
136+
DocumentNotInProject(String),
132137
}
133138

134139
impl Error {
@@ -137,6 +142,7 @@ impl Error {
137142
pub fn path(&self) -> Option<&String> {
138143
match self {
139144
Error::GitHubManifestParse { path, .. }
145+
| Error::DocumentNotInProject(path)
140146
| Error::NoSrcDir { path }
141147
| Error::ManifestParse { path, .. } => Some(path),
142148
// Note we don't return the path for `FileSystem` errors,
@@ -182,10 +188,7 @@ pub trait FileSystemAsync {
182188
) -> miette::Result<Arc<str>>;
183189

184190
/// Given an initial path, fetch files matching <initial_path>/**/*.qs
185-
async fn collect_project_sources(
186-
&self,
187-
initial_path: &Path,
188-
) -> ProjectResult<Vec<Self::Entry>> {
191+
async fn collect_project_sources(&self, initial_path: &Path) -> ProjectResult<Vec<PathBuf>> {
189192
let listing = self
190193
.list_directory(initial_path)
191194
.await
@@ -210,7 +213,7 @@ pub trait FileSystemAsync {
210213
async fn collect_project_sources_inner(
211214
&self,
212215
initial_path: &Path,
213-
) -> ProjectResult<Vec<Self::Entry>> {
216+
) -> ProjectResult<Vec<PathBuf>> {
214217
let listing = self
215218
.list_directory(initial_path)
216219
.await
@@ -221,7 +224,7 @@ pub trait FileSystemAsync {
221224
let mut files = vec![];
222225
for item in filter_hidden_files(listing.into_iter()) {
223226
match item.entry_type() {
224-
Ok(EntryType::File) if item.entry_extension() == "qs" => files.push(item),
227+
Ok(EntryType::File) if item.entry_extension() == "qs" => files.push(item.path()),
225228
Ok(EntryType::Folder) => {
226229
files.append(&mut self.collect_project_sources_inner(&item.path()).await?);
227230
}
@@ -231,8 +234,64 @@ pub trait FileSystemAsync {
231234
Ok(files)
232235
}
233236

237+
async fn collect_sources_from_files_field(
238+
&self,
239+
project_path: &Path,
240+
manifest: &Manifest,
241+
) -> ProjectResult<Vec<PathBuf>> {
242+
let mut v = vec![];
243+
for file in &manifest.files {
244+
v.push(
245+
self.resolve_path(project_path, Path::new(&file))
246+
.await
247+
.map_err(|e| Error::FileSystem {
248+
about_path: project_path.to_string_lossy().to_string(),
249+
error: e.to_string(),
250+
})?,
251+
);
252+
}
253+
Ok(v)
254+
}
255+
256+
fn validate(
257+
&self,
258+
qs_files: &mut Vec<PathBuf>,
259+
listed_files: &mut Vec<PathBuf>,
260+
) -> Result<(), Vec<Error>> {
261+
qs_files.sort();
262+
listed_files.sort();
263+
264+
// If the `files` field exists in the manifest, validate it includes
265+
// all the files in the `src` directory.
266+
// how do I subtract one sorted vector from another
267+
let mut difference = qs_files.clone();
268+
let mut iter2 = listed_files.iter().peekable();
269+
270+
difference.retain(|item| {
271+
while let Some(&next) = iter2.peek() {
272+
if next < item {
273+
iter2.next();
274+
} else {
275+
break;
276+
}
277+
}
278+
iter2.peek() != Some(&item)
279+
});
280+
281+
if !difference.is_empty() {
282+
return Err(difference
283+
.iter()
284+
.map(|p| Error::DocumentNotInProject(p.to_string_lossy().to_string()))
285+
.collect());
286+
}
287+
Ok(())
288+
}
289+
234290
/// Given a directory, loads the project sources
235291
/// and the sources for all its dependencies.
292+
///
293+
/// Any errors that didn't block project load are contained in the
294+
/// `errors` field of the returned `Project`.
236295
async fn load_project(
237296
&self,
238297
directory: &Path,
@@ -243,15 +302,15 @@ pub trait FileSystemAsync {
243302
.await
244303
.map_err(|e| vec![e])?;
245304

246-
let root = self
247-
.read_local_manifest_and_sources(directory)
248-
.await
249-
.map_err(|e| vec![e])?;
250-
251305
let mut errors = vec![];
252306
let mut packages = FxHashMap::default();
253307
let mut stack = vec![];
254308

309+
let root = self
310+
.read_local_manifest_and_sources(directory, &mut errors)
311+
.await
312+
.map_err(|e| vec![e])?;
313+
255314
let root_path = directory.to_string_lossy().to_string();
256315
let root_ref = PackageRef::Path { path: root_path };
257316

@@ -321,41 +380,33 @@ pub trait FileSystemAsync {
321380

322381
/// Load the sources for a single package at the given directory. Also load its
323382
/// dependency information but don't recurse into dependencies yet.
383+
///
384+
/// Any errors that didn't block project load are accumulated into the `errors` vector.
324385
async fn read_local_manifest_and_sources(
325386
&self,
326-
directory: &Path,
387+
manifest_dir: &Path,
388+
errors: &mut Vec<Error>,
327389
) -> ProjectResult<PackageInfo> {
328-
let manifest = self.parse_manifest_in_dir(directory).await?;
329-
330-
let manifest = ManifestDescriptor {
331-
manifest_dir: directory.to_path_buf(),
332-
manifest,
333-
};
334-
335-
let project_path = manifest.manifest_dir.clone();
336-
337-
// If the `files` field exists in the manifest, prefer that.
338-
// Otherwise, collect all files in the project directory.
339-
let qs_files: Vec<PathBuf> = if manifest.manifest.files.is_empty() {
340-
let qs_files = self.collect_project_sources(&project_path).await?;
341-
qs_files.into_iter().map(|file| file.path()).collect()
342-
} else {
343-
let mut v = vec![];
344-
for file in manifest.manifest.files {
345-
v.push(
346-
self.resolve_path(&project_path, Path::new(&file))
347-
.await
348-
.map_err(|e| Error::FileSystem {
349-
about_path: project_path.to_string_lossy().to_string(),
350-
error: e.to_string(),
351-
})?,
352-
);
353-
}
354-
v
355-
};
390+
let manifest = self.parse_manifest_in_dir(manifest_dir).await?;
391+
392+
// All the *.qs files under src/
393+
let mut all_qs_files = self.collect_project_sources(manifest_dir).await?;
394+
395+
// Files explicitly listed in the `files` field of the manifest
396+
let mut listed_files = self
397+
.collect_sources_from_files_field(manifest_dir, &manifest)
398+
.await?;
399+
400+
if !listed_files.is_empty() {
401+
errors.extend(
402+
self.validate(&mut all_qs_files, &mut listed_files)
403+
.err()
404+
.unwrap_or_default(),
405+
);
406+
}
356407

357-
let mut sources = Vec::with_capacity(qs_files.len());
358-
for path in qs_files {
408+
let mut sources = Vec::with_capacity(all_qs_files.len());
409+
for path in all_qs_files {
359410
sources.push(self.read_file(&path).await.map_err(|e| Error::FileSystem {
360411
about_path: path.to_string_lossy().to_string(),
361412
error: e.to_string(),
@@ -367,13 +418,13 @@ pub trait FileSystemAsync {
367418
// For any local dependencies, convert relative paths to absolute,
368419
// so that multiple references to the same package, from different packages,
369420
// get merged correctly.
370-
for (alias, mut dep) in manifest.manifest.dependencies {
421+
for (alias, mut dep) in manifest.dependencies {
371422
if let PackageRef::Path { path: dep_path } = &mut dep {
372423
*dep_path = self
373-
.resolve_path(&project_path, &PathBuf::from(dep_path.clone()))
424+
.resolve_path(manifest_dir, &PathBuf::from(dep_path.clone()))
374425
.await
375426
.map_err(|e| Error::FileSystem {
376-
about_path: project_path.to_string_lossy().to_string(),
427+
about_path: manifest_dir.to_string_lossy().to_string(),
377428
error: e.to_string(),
378429
})?
379430
.to_string_lossy()
@@ -384,9 +435,9 @@ pub trait FileSystemAsync {
384435

385436
Ok(PackageInfo {
386437
sources,
387-
language_features: LanguageFeatures::from_iter(&manifest.manifest.language_features),
438+
language_features: LanguageFeatures::from_iter(manifest.language_features),
388439
dependencies,
389-
package_type: manifest.manifest.package_type,
440+
package_type: manifest.package_type,
390441
})
391442
}
392443

@@ -477,6 +528,7 @@ pub trait FileSystemAsync {
477528
global_cache: &RefCell<PackageCache>,
478529
key: PackageKey,
479530
this_pkg: &PackageRef,
531+
errors: &mut Vec<Error>,
480532
) -> ProjectResult<PackageInfo> {
481533
match this_pkg {
482534
PackageRef::GitHub { github } => {
@@ -499,7 +551,7 @@ pub trait FileSystemAsync {
499551
// editing experience as intuitive as possible. This may change if we start
500552
// hitting perf issues, but careful consideration is needed into when to
501553
// invalidate the cache.
502-
self.read_local_manifest_and_sources(PathBuf::from(path.clone()).as_path())
554+
self.read_local_manifest_and_sources(PathBuf::from(path.clone()).as_path(), errors)
503555
.await
504556
}
505557
}
@@ -535,7 +587,7 @@ pub trait FileSystemAsync {
535587
}
536588

537589
let dep_result = self
538-
.read_manifest_and_sources(global_cache, dep_key.clone(), &dependency)
590+
.read_manifest_and_sources(global_cache, dep_key.clone(), &dependency, errors)
539591
.await;
540592

541593
match dep_result {

compiler/qsc_project/src/tests.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,10 @@ fn explicit_files_list() {
426426
"explicit_files_list/src/Main.qs",
427427
"namespace Dependency {\n function LibraryFn() : Unit {\n }\n}\n",
428428
),
429+
(
430+
"explicit_files_list/src/NotIncluded.qs",
431+
"namespace Dependency {\n function LibraryFn() : Unit {\n }\n}\n",
432+
),
429433
],
430434
language_features: LanguageFeatures(
431435
0,
@@ -436,7 +440,11 @@ fn explicit_files_list() {
436440
packages: {},
437441
},
438442
lints: [],
439-
errors: [],
443+
errors: [
444+
DocumentNotInProject(
445+
"explicit_files_list/src/NotIncluded.qs",
446+
),
447+
],
440448
}"#]],
441449
);
442450
}

compiler/qsc_project/src/tests/harness.rs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ fn normalize(project: &mut Project, root_path: &Path) {
5959
match err {
6060
Error::NoSrcDir { path }
6161
| Error::ManifestParse { path, .. }
62-
| Error::GitHubManifestParse { path, .. } => {
62+
| Error::GitHubManifestParse { path, .. }
63+
| Error::DocumentNotInProject(path) => {
6364
let mut str = std::mem::take(path).into();
6465
remove_absolute_path_prefix(&mut str, root_path);
6566
*path = str.to_string();
@@ -74,13 +75,11 @@ fn normalize(project: &mut Project, root_path: &Path) {
7475
*error = "REPLACED".to_string();
7576
}
7677
Error::Circular(s1, s2) | Error::GitHubToLocal(s1, s2) => {
77-
// These errors contain absolute paths which don't work well in test output
78+
// these strings can contain mangled absolute paths so we can't fix them
7879
*s1 = "REPLACED".to_string();
7980
*s2 = "REPLACED".to_string();
8081
}
81-
Error::GitHub(s) => {
82-
*s = "REPLACED".to_string();
83-
}
82+
Error::GitHub(_) => {}
8483
}
8584
}
8685
}

0 commit comments

Comments
 (0)