@@ -107,6 +107,11 @@ func WithAllowIncludeOfImportedType() ImageFilterOption {
107107// For example, "google.protobuf.Any" or "buf.validate". Types may be nested,
108108// and can be any package, message, enum, extension, service or method name.
109109//
110+ // A name ending in ".**" is a recursive glob that matches the named element and
111+ // every symbol nested beneath it. For example, "acme.foo.**" matches the package
112+ // "acme.foo", all of its sub-packages, and all of their types; "acme.Foo.**"
113+ // matches the message "acme.Foo" and all of its nested types.
114+ //
110115// If the type does not exist in the image, an error wrapping
111116// [ErrImageFilterTypeNotFound] will be returned.
112117func WithIncludeTypes (typeNames ... string ) ImageFilterOption {
@@ -127,6 +132,11 @@ func WithIncludeTypes(typeNames ...string) ImageFilterOption {
127132// For example, "google.protobuf.Any" or "buf.validate". Types may be nested,
128133// and can be any package, message, enum, extension, service or method name.
129134//
135+ // Exclusion is always recursive: excluding a name removes the named element and
136+ // every symbol nested beneath it. For example, excluding a package removes the
137+ // package, all of its sub-packages, and all of their types. A trailing ".**" is
138+ // accepted for symmetry with [WithIncludeTypes] but has no additional effect.
139+ //
130140// If the type does not exist in the image, an error wrapping
131141// [ErrImageFilterTypeNotFound] will be returned.
132142func WithExcludeTypes (typeNames ... string ) ImageFilterOption {
@@ -345,6 +355,7 @@ func (t *transitiveClosure) hasOption(
345355
346356func (t * transitiveClosure ) includeType (
347357 typeName protoreflect.FullName ,
358+ recursive bool ,
348359 imageIndex * imageIndex ,
349360 options * imageFilterOptions ,
350361) error {
@@ -372,6 +383,12 @@ func (t *transitiveClosure) includeType(
372383 if err := t .addElement (descriptorInfo .element , "" , false , imageIndex , options ); err != nil {
373384 return fmt .Errorf ("inclusion of type %q: %w" , typeName , err )
374385 }
386+ if recursive {
387+ // Auto-include any symbol nested beneath the named element.
388+ if err := t .includeChildElements (descriptorInfo .element , imageIndex , options ); err != nil {
389+ return fmt .Errorf ("inclusion of type %q: %w" , typeName , err )
390+ }
391+ }
375392 return nil
376393 }
377394 // It could be a package name
@@ -381,9 +398,14 @@ func (t *transitiveClosure) includeType(
381398 return fmt .Errorf ("inclusion of type %q: %w" , typeName , ErrImageFilterTypeNotFound )
382399 }
383400 if ! options .allowImportedTypes {
384- // if package includes only imported files, then reject
401+ // If the package contains only imported files, then reject. For a
402+ // recursive glob, sub-packages count toward this check too.
403+ packageFiles := pkg .files
404+ if recursive {
405+ packageFiles = appendPackageFiles (nil , pkg )
406+ }
385407 onlyImported := true
386- for _ , file := range pkg . files {
408+ for _ , file := range packageFiles {
387409 if ! file .IsImport () {
388410 onlyImported = false
389411 break
@@ -393,18 +415,91 @@ func (t *transitiveClosure) includeType(
393415 return fmt .Errorf ("inclusion of type %q: %w" , typeName , ErrImageFilterTypeIsImport )
394416 }
395417 }
418+ return t .includePackage (pkg , recursive , true , imageIndex , options )
419+ }
420+
421+ // includePackage adds the files of the given package to the closure, recursing
422+ // into sub-packages when recursive is set.
423+ //
424+ // The top-level (named) package is an error if it has been excluded: including
425+ // and excluding the same package expand to the same set and cancel out, exactly
426+ // as for a non-recursive package include. Sub-packages reached only via the
427+ // recursive glob are instead skipped when excluded, so that e.g. including
428+ // "a.b.**" while excluding "a.b.c" succeeds.
429+ func (t * transitiveClosure ) includePackage (
430+ pkg * packageInfo ,
431+ recursive bool ,
432+ isTopLevel bool ,
433+ imageIndex * imageIndex ,
434+ options * imageFilterOptions ,
435+ ) error {
396436 for _ , file := range pkg .files {
397437 fileDescriptor := file .FileDescriptorProto ()
398438 if mode := t .elements [fileDescriptor ]; mode == inclusionModeExcluded {
399- return fmt .Errorf ("inclusion of excluded package %q" , typeName )
439+ if ! isTopLevel {
440+ continue
441+ }
442+ return fmt .Errorf ("inclusion of excluded package %q" , pkg .fullName )
400443 }
401444 if err := t .addElement (fileDescriptor , "" , false , imageIndex , options ); err != nil {
402- return fmt .Errorf ("inclusion of type %q: %w" , typeName , err )
445+ return fmt .Errorf ("inclusion of type %q: %w" , pkg .fullName , err )
446+ }
447+ }
448+ if ! recursive {
449+ return nil
450+ }
451+ for _ , subPackage := range pkg .subPackages {
452+ if err := t .includePackage (subPackage , recursive , false , imageIndex , options ); err != nil {
453+ return err
454+ }
455+ }
456+ return nil
457+ }
458+
459+ // includeChildElements recursively adds the symbols nested beneath the given
460+ // element to the closure. Only messages have such children: their nested
461+ // messages and enums. Other element kinds have nothing to expand.
462+ func (t * transitiveClosure ) includeChildElements (
463+ descriptor namedDescriptor ,
464+ imageIndex * imageIndex ,
465+ options * imageFilterOptions ,
466+ ) error {
467+ message , ok := descriptor .(* descriptorpb.DescriptorProto )
468+ if ! ok {
469+ return nil
470+ }
471+ for _ , nestedMessage := range message .GetNestedType () {
472+ if mode := t .elements [nestedMessage ]; mode == inclusionModeExcluded {
473+ continue
474+ }
475+ if err := t .addElement (nestedMessage , "" , false , imageIndex , options ); err != nil {
476+ return err
477+ }
478+ if err := t .includeChildElements (nestedMessage , imageIndex , options ); err != nil {
479+ return err
480+ }
481+ }
482+ for _ , nestedEnum := range message .GetEnumType () {
483+ if mode := t .elements [nestedEnum ]; mode == inclusionModeExcluded {
484+ continue
485+ }
486+ if err := t .addElement (nestedEnum , "" , false , imageIndex , options ); err != nil {
487+ return err
403488 }
404489 }
405490 return nil
406491}
407492
493+ // appendPackageFiles appends the files of the given package and all of its
494+ // sub-packages, recursively, to files.
495+ func appendPackageFiles (files []bufimage.ImageFile , pkg * packageInfo ) []bufimage.ImageFile {
496+ files = append (files , pkg .files ... )
497+ for _ , subPackage := range pkg .subPackages {
498+ files = appendPackageFiles (files , subPackage )
499+ }
500+ return files
501+ }
502+
408503func (t * transitiveClosure ) addImport (fromPath , toPath string ) {
409504 if _ , ok := t .imports [toPath ]; ! ok {
410505 t .imports [toPath ] = nil // mark as seen
@@ -616,6 +711,8 @@ func (t *transitiveClosure) excludeType(
616711) error {
617712 descriptorInfo , ok := imageIndex .ByName [typeName ]
618713 if ok {
714+ // excludeElement recurses into all nested elements: a symbol cannot be
715+ // removed while keeping the types nested within it.
619716 return t .excludeElement (descriptorInfo .element , imageIndex , options )
620717 }
621718 // It could be a package name
@@ -624,8 +721,13 @@ func (t *transitiveClosure) excludeType(
624721 // but it's not...
625722 return fmt .Errorf ("exclusion of type %q: %w" , typeName , ErrImageFilterTypeNotFound )
626723 }
627- // Exclude the package and all of its files.
628- for _ , file := range pkg .files {
724+ // Exclude the package, all of its files, and all of its sub-packages.
725+ // TODO: Exclusion is always recursive. Ideally a name without a ".**" suffix
726+ // would exclude only the named package's own files (not its sub-packages),
727+ // matching the include semantics. Making non-glob exclusion non-recursive
728+ // is an incompatible behavior change that would require auditing and
729+ // updating all callers, so it is deferred.
730+ for _ , file := range appendPackageFiles (nil , pkg ) {
629731 fileDescriptor := file .FileDescriptorProto ()
630732 if err := t .excludeElement (fileDescriptor , imageIndex , options ); err != nil {
631733 return err
0 commit comments