1+ use std:: collections:: HashSet ;
12use std:: path:: PathBuf ;
23use std:: { ops:: Range , path:: Path , str:: FromStr } ;
34
@@ -27,22 +28,16 @@ pub struct Worlds {
2728#[ derive( Debug , Clone ) ]
2829pub 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 ) ]
4136pub 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.
490485fn 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
535619fn 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(
607691fn 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 {
0 commit comments