Skip to content

Commit 5c3747c

Browse files
committed
Fix how excluded files are determined
1 parent 2501358 commit 5c3747c

6 files changed

Lines changed: 141 additions & 111 deletions

File tree

src/check.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ mod path;
1919
mod readme;
2020

2121
pub use diagnostics::{Diagnostics, Result, TryExt};
22+
pub use manifest::Exclude;
2223

2324
pub async fn all_checks(
2425
package_spec: Option<&PackageSpec>,

src/check/files.rs

Lines changed: 3 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
use std::collections::HashSet;
21
use std::path::Path;
32

43
use codespan_reporting::diagnostic::{Diagnostic, Label};
@@ -16,35 +15,17 @@ pub fn check(
1615
readme: &Option<Readme>,
1716
) {
1817
let exclude = &manifest.package.exclude;
19-
let thumbnail_path = manifest.thumbnail();
20-
21-
// Manually keep track of excluded directories, to figure out if nested
22-
// files are ignored. This is done, so we can generate diagnostics for
23-
// excluded files.
24-
let mut excluded_dirs = HashSet::new();
2518

2619
for ch in WalkDir::new(package_dir).into_iter().flatten() {
2720
let Ok(metadata) = ch.metadata() else {
2821
continue;
2922
};
30-
31-
let file_path = PackagePath::from_full(package_dir, ch.path());
32-
33-
if metadata.is_dir() {
34-
// If the parent directory is ignored, all children are ignored too.
35-
if parent_is_excluded(&excluded_dirs, file_path)
36-
|| exclude.matched(file_path.relative(), true).is_ignore()
37-
{
38-
excluded_dirs.insert(ch.into_path());
39-
}
23+
if !metadata.is_file() {
4024
continue;
4125
}
4226

43-
// The thumbnail is always excluded.
44-
let is_thumbnail = thumbnail_path.is_some_and(|t| t.val == file_path);
45-
let excluded = is_thumbnail
46-
|| parent_is_excluded(&excluded_dirs, file_path)
47-
|| exclude.matched(file_path.relative(), false).is_ignore();
27+
let file_path = PackagePath::from_full(package_dir, ch.path());
28+
let excluded = exclude.matches_file(&file_path);
4829

4930
forbid_font_files(diags, file_path);
5031
exclude_large_files(diags, file_path, excluded, metadata.len());
@@ -53,16 +34,6 @@ pub fn check(
5334
}
5435
}
5536

56-
fn parent_is_excluded(
57-
excluded_dirs: &HashSet<std::path::PathBuf>,
58-
file_path: PackagePath<&Path>,
59-
) -> bool {
60-
file_path
61-
.full()
62-
.parent()
63-
.is_some_and(|parent| excluded_dirs.contains(parent))
64-
}
65-
6637
fn exclude_large_files(
6738
diags: &mut Diagnostics,
6839
path: PackagePath<&Path>,

src/check/manifest.rs

Lines changed: 106 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use std::collections::HashSet;
12
use std::path::PathBuf;
23
use std::{ops::Range, path::Path, str::FromStr};
34

@@ -27,22 +28,16 @@ pub struct Worlds {
2728
#[derive(Debug, Clone)]
2829
pub struct Manifest {
2930
pub package: Spanned<Package>,
31+
#[allow(unused)]
3032
pub template: Option<Spanned<Template>>,
3133
}
3234

33-
impl Manifest {
34-
pub fn thumbnail(&self) -> Option<Spanned<PackagePath<&Path>>> {
35-
let thumbnail = self.template.as_ref()?.thumbnail.as_ref()?;
36-
Some(thumbnail.as_ref().map(PackagePath::as_path))
37-
}
38-
}
39-
4035
#[derive(Debug, Clone)]
4136
pub struct Package {
4237
pub entrypoint: Spanned<PackagePath>,
4338
pub name: Option<Spanned<String>>,
4439
pub version: Option<Spanned<PackageVersion>>,
45-
pub exclude: Spanned<Override>,
40+
pub exclude: Spanned<Exclude>,
4641
}
4742

4843
#[derive(Debug, Clone)]
@@ -89,7 +84,8 @@ pub async fn check(
8984
let entrypoint = entrypoint.map(|e| PackagePath::from_relative(package_dir, e));
9085
let name = check_name(diags, package, package_spec);
9186
let version = check_version(diags, package, package_spec);
92-
let exclude = check_exclude(diags, package, package_dir)?;
87+
let template = check_template(diags, &manifest, package_dir);
88+
let exclude = check_exclude(diags, package_dir, package, &template)?;
9389

9490
check_compiler_version(diags, package);
9591
check_universe_fields(diags, package);
@@ -108,9 +104,8 @@ pub async fn check(
108104
exclude,
109105
});
110106

111-
let template = check_template(diags, &manifest, package_dir);
112107
if let Some(template) = &template {
113-
check_thumbnail(diags, &package.exclude, template);
108+
check_thumbnail(diags, template);
114109
dont_exclude_template_files(diags, package_dir, &package.exclude, template);
115110
}
116111

@@ -285,10 +280,10 @@ fn check_compiler_version(diags: &mut Diagnostics, package: Spanned<&Table>) ->
285280
Some(())
286281
}
287282

288-
fn dont_over_exclude(diags: &mut Diagnostics, exclude: &Spanned<Override>) -> Result<()> {
283+
fn dont_over_exclude(diags: &mut Diagnostics, exclude: &Spanned<Exclude>) -> Result<()> {
289284
let warning = Diagnostic::warning().with_label(Label::primary(manifest_id(), exclude.span()));
290285

291-
if exclude.matched("LICENSE", false).is_ignore() {
286+
if exclude.matches_relative_file("LICENSE") {
292287
diags.emit(
293288
warning
294289
.clone()
@@ -297,7 +292,7 @@ fn dont_over_exclude(diags: &mut Diagnostics, exclude: &Spanned<Override>) -> Re
297292
);
298293
}
299294

300-
if exclude.matched("README.md", false).is_ignore() {
295+
if exclude.matches_relative_file("README.md") {
301296
diags.emit(
302297
warning
303298
.with_code("exclude/readme")
@@ -489,11 +484,12 @@ async fn check_repo(diags: &mut Diagnostics, package: Spanned<&Table>) {
489484
/// a lot of false positives in other diagnostics.
490485
fn check_exclude(
491486
diags: &mut Diagnostics,
492-
package: Spanned<&Table>,
493487
package_dir: &Path,
494-
) -> Result<Spanned<Override>> {
488+
package: Spanned<&Table>,
489+
template: &Option<Spanned<Template>>,
490+
) -> Result<Spanned<Exclude>> {
495491
let Some(exclude) = package.get_spanned("exclude") else {
496-
return Ok(Spanned::new(Override::empty(), package.span()));
492+
return Ok(Spanned::new(Exclude::empty(), package.span()));
497493
};
498494

499495
let exclude = exclude.try_map(Item::as_array).error(
@@ -526,10 +522,98 @@ fn check_exclude(
526522
exclude_globs.add(&format!("!{exclusion_str}")).ok();
527523
}
528524

525+
let thumbnail_path = template.as_ref().and_then(|t| t.thumbnail.as_ref());
526+
if let Some(thumbnail_path) = thumbnail_path {
527+
// Check if the thumbnail is excluded.
528+
if let Ok(exclude) = exclude_globs.build()
529+
&& exclude.matched(thumbnail_path.full(), false).is_ignore()
530+
{
531+
diags.emit(
532+
Diagnostic::error()
533+
.with_label(Label::primary(manifest_id(), thumbnail_path.span()))
534+
.with_code("manifest/template/thumbnail/exclude")
535+
.with_message("The template thumbnail is automatically excluded"),
536+
);
537+
}
538+
539+
exclude_globs
540+
.add(&format!("!{}", thumbnail_path.relative().display()))
541+
.ok();
542+
}
543+
529544
let exclude_globs = exclude_globs
530545
.build()
531546
.error("manifest/exclude/invalid", "Invalid exclude globs")?;
532-
Ok(exclude.map(|_| exclude_globs))
547+
548+
Ok(exclude.map(|_| Exclude::index(exclude_globs)))
549+
}
550+
551+
#[derive(Debug, Clone)]
552+
pub struct Exclude {
553+
globs: Override,
554+
/// Manually keep track of excluded directories, to figure out if nested
555+
/// files are ignored. This has to be done to mimic the behavior of
556+
/// [`ignore::Walk`], which won't enter a directory if it is ignored.
557+
/// The bare [`Override::matched] function doesn't check if a parent
558+
/// directory is excluded.
559+
excluded_dirs: HashSet<PathBuf>,
560+
}
561+
562+
impl Exclude {
563+
pub fn empty() -> Self {
564+
Self {
565+
globs: Override::empty(),
566+
excluded_dirs: HashSet::new(),
567+
}
568+
}
569+
570+
pub fn index(globs: Override) -> Self {
571+
let mut excluded_dirs = HashSet::new();
572+
573+
for ch in WalkDir::new(globs.path()).into_iter().flatten() {
574+
let Ok(metadata) = ch.metadata() else {
575+
continue;
576+
};
577+
if !metadata.is_dir() {
578+
continue;
579+
}
580+
581+
let relative_path = ch
582+
.path()
583+
.strip_prefix(globs.path())
584+
.expect("Path should be inside the package dir");
585+
let parent_is_excluded = relative_path
586+
.parent()
587+
.is_some_and(|parent| excluded_dirs.contains(parent));
588+
589+
// If the parent directory is ignored, all children are ignored too.
590+
if parent_is_excluded || globs.matched(relative_path, true).is_ignore() {
591+
excluded_dirs.insert(relative_path.to_path_buf());
592+
}
593+
}
594+
595+
Self {
596+
globs,
597+
excluded_dirs,
598+
}
599+
}
600+
601+
/// Whether the package file path is excluded.
602+
pub fn matches_file<T: AsRef<Path>>(&self, path: &PackagePath<T>) -> bool {
603+
self.matches_relative_file(path.relative())
604+
}
605+
606+
/// Whether the package relative file path is excluded.
607+
pub fn matches_relative_file(&self, relative_path: impl AsRef<Path>) -> bool {
608+
let relative_path = relative_path.as_ref();
609+
if self.globs.matched(relative_path, false).is_ignore() {
610+
return true;
611+
}
612+
613+
relative_path
614+
.parent()
615+
.is_some_and(|parent| self.excluded_dirs.contains(parent))
616+
}
533617
}
534618

535619
fn check_template(
@@ -558,7 +642,7 @@ fn check_template(
558642
let template_dir = path.as_ref()?;
559643
Some(
560644
entrypoint
561-
.map(|entrypoint| path::relative_to(template_dir.full(), entrypoint))
645+
.map(|entrypoint| path::join_to(template_dir.full(), entrypoint))
562646
.map(|path| PackagePath::from_full(package_dir, path)),
563647
)
564648
});
@@ -607,7 +691,7 @@ fn world_for_template(
607691
fn dont_exclude_template_files(
608692
diags: &mut Diagnostics,
609693
package_dir: &Path,
610-
exclude: &Override,
694+
exclude: &Exclude,
611695
template: &Spanned<Template>,
612696
) {
613697
let Some(template_path) = &template.path else {
@@ -635,10 +719,7 @@ fn dont_exclude_template_files(
635719
}
636720

637721
// For other files, check that they are indeed not excluded.
638-
if exclude
639-
.matched(entry.path(), entry.metadata().is_ok_and(|m| m.is_dir()))
640-
.is_ignore()
641-
{
722+
if entry.metadata().is_ok_and(|m| m.is_file()) && exclude.matches_file(&entry_path) {
642723
diags.emit(
643724
Diagnostic::error()
644725
.with_code("exclude/template")
@@ -649,7 +730,7 @@ fn dont_exclude_template_files(
649730
}
650731
}
651732

652-
fn check_thumbnail(diags: &mut Diagnostics, exclude: &Override, template: &Spanned<Template>) {
733+
fn check_thumbnail(diags: &mut Diagnostics, template: &Spanned<Template>) {
653734
let Some(thumbnail_path) = &template.thumbnail else {
654735
return;
655736
};
@@ -675,15 +756,6 @@ fn check_thumbnail(diags: &mut Diagnostics, exclude: &Override, template: &Spann
675756
)
676757
}
677758

678-
if exclude.matched(thumbnail_path.full(), false).is_ignore() {
679-
diags.emit(
680-
Diagnostic::error()
681-
.with_label(Label::primary(manifest_id(), thumbnail_path.span()))
682-
.with_code("manifest/template/thumbnail/exclude")
683-
.with_message("The template thumbnail is automatically excluded"),
684-
);
685-
}
686-
687759
if let Some(template_path) = &template.path
688760
&& thumbnail_path.full().starts_with(template_path.full())
689761
{

src/check/path.rs

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,10 @@ impl PackagePath<PathBuf> {
1515
/// Create a new package path from the package directory and a relative path
1616
/// within the directory.
1717
pub fn from_relative<T: AsRef<Path>>(package_dir: &Path, relative_path: T) -> Self {
18-
let full_path = relative_to(package_dir, relative_path.as_ref());
18+
let full_path = join_to(package_dir, relative_path.as_ref());
1919
let offset = package_dir.components().count();
2020
Self { offset, full_path }
2121
}
22-
23-
pub fn as_path(&self) -> PackagePath<&Path> {
24-
PackagePath {
25-
offset: self.offset,
26-
full_path: self.full_path.as_path(),
27-
}
28-
}
2922
}
3023

3124
impl<T: AsRef<Path>> PackagePath<T> {
@@ -76,7 +69,7 @@ impl<T: AsRef<Path>> PackagePath<T> {
7669
/// Strips any any leading root components (`/` or `\`) of the `path` before
7770
/// joining it to the `root` path. Absolute paths would otherwise replace the
7871
/// complete path when `join`ed with a parent path.
79-
pub fn relative_to(root: &Path, path: impl AsRef<Path>) -> PathBuf {
72+
pub fn join_to(root: &Path, path: impl AsRef<Path>) -> PathBuf {
8073
let components = path
8174
.as_ref()
8275
.components()

src/cli.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ use std::path::PathBuf;
22
use std::process::exit;
33

44
use codespan_reporting::{diagnostic::Diagnostic, term};
5-
use ignore::overrides::Override;
65
use tracing::error;
76
use typst::syntax::{FileId, Source, package::PackageSpec};
87

8+
use crate::check::Exclude;
99
use crate::{check::all_checks, package::PackageExt, world::SystemWorld};
1010

1111
pub async fn main(spec_or_path: String, json_output: bool) {
@@ -59,7 +59,7 @@ pub fn print_diagnostics(
5959
// We should be able to print diagnostics even on excluded files. If we
6060
// don't remove the exclusion, it will fail to read and display the file
6161
// contents.
62-
world.exclude(Override::empty());
62+
world.exclude(Exclude::empty());
6363

6464
for diagnostic in warnings.iter().chain(errors) {
6565
if json {

0 commit comments

Comments
 (0)