Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@

- Fix LSP hover and highlighting of CEL expressions whose proto strings contain escape sequences,
which placed the ranges of string literals, and of tokens after a `\u` or `\U` escape, wrongly.
- Fix `buf build --type=X` filters to correctly mark non-targeted files as imports. The `is_import`
comment in `image.proto` is also updated to describe this. Image filtering now has a unified order
across commands, where `--path` applies first, then `--type`, then `--exclude-imports`. Some
consequences:
- `buf build --type a.A --exclude-imports` outputs only the file that defines `a.A`, and no longer
fails when `a.A` has a field whose type is defined in a dependency or a Well-Known Type.
- `buf generate` with `types` no longer generates code for files kept only by reference. To
generate them again, set `include_imports: true` on the plugin or add their types.
- `--path` now gives the same result for an image input as for a source input, with or without
`--type`.

## [v1.75.0] - 2026-10-21

Expand Down
146 changes: 146 additions & 0 deletions cmd/buf/buf_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2559,6 +2559,152 @@ func TestBuildOverlappingPaths(t *testing.T) {
)
}

func TestBuildTypeWithExcludeImports(t *testing.T) {
t.Parallel()
testBuildLsFilesFormatImport(
t,
0,
[]string{"a.proto"},
filepath.Join("testdata", "imports", "success", "wkt"),
"--type",
"A",
"--exclude-imports",
)
}

func TestBuildPathWithExcludeImportsFromImage(t *testing.T) {
t.Parallel()
tempDir := t.TempDir()
imagePath := filepath.Join(tempDir, "image.binpb")
testRunStdout(
t,
nil,
0,
``,
"build",
filepath.Join("testdata", "imports", "success", "sibling"),
"-o",
imagePath,
)
testBuildLsFilesFormatImport(
t,
0,
[]string{"a.proto"},
imagePath,
"--path",
"a.proto",
"--exclude-imports",
)
}

func TestBuildTypeMarksReferencedFileAsImport(t *testing.T) {
t.Parallel()
// a.proto imports b.proto from the same module. Including only A keeps
// b.proto for B, but marks it as an import because it contains no
// included type, so --exclude-imports drops it.
testBuildLsFilesFormatImport(
t,
0,
[]string{"a.proto"},
filepath.Join("testdata", "imports", "success", "sibling"),
"--type",
"A",
"--exclude-imports",
)
}

func TestBuildTypeWithPathMarksReferencedFileAsImport(t *testing.T) {
t.Parallel()
// Image building/filtering takes the intersection of `--path` and `--type`.
testBuildLsFilesFormatImport(
t,
0,
[]string{"a.proto"},
filepath.Join("testdata", "imports", "success", "sibling"),
"--type",
"A",
"--path",
filepath.Join("testdata", "imports", "success", "sibling", "a.proto"),
"--path",
filepath.Join("testdata", "imports", "success", "sibling", "b.proto"),
"--exclude-imports",
)
}

func TestBuildTypeWithPathMarksReferencedFileAsImportFromImage(t *testing.T) {
t.Parallel()
// Same flags as TestBuildTypeWithPathMarksReferencedFileAsImport, but with image input.
tempDir := t.TempDir()
imagePath := filepath.Join(tempDir, "image.binpb")
testRunStdout(
t,
nil,
0,
``,
"build",
filepath.Join("testdata", "imports", "success", "sibling"),
"-o",
imagePath,
)
testBuildLsFilesFormatImport(
t,
0,
[]string{"a.proto"},
imagePath,
"--type",
"A",
"--path",
"a.proto",
"--path",
"b.proto",
"--exclude-imports",
)
}

func TestBuildTypeOutsidePathFromImage(t *testing.T) {
t.Parallel()
// It's an error when the intersection between `--path` and `--type` is empty.
tempDir := t.TempDir()
imagePath := filepath.Join(tempDir, "image.binpb")
testRunStdout(
t,
nil,
0,
``,
"build",
filepath.Join("testdata", "imports", "success", "sibling"),
"-o",
imagePath,
)
testRunStderrContainsNoWarn(
t,
nil,
1,
[]string{`inclusion of type "A": not found`},
"build",
imagePath,
"--path",
"b.proto",
"--type",
"A",
)
}

func TestBuildPackageTypeWithPathKeepsIntersection(t *testing.T) {
t.Parallel()
testBuildLsFilesFormatImport(
t,
0,
[]string{"a.proto"},
filepath.Join("testdata", "imports", "success", "samepkg"),
"--type",
"pkg",
"--path",
filepath.Join("testdata", "imports", "success", "samepkg", "a.proto"),
"--exclude-imports",
)
}

func TestExportProto(t *testing.T) {
t.Parallel()
tempDir := t.TempDir()
Expand Down
3 changes: 0 additions & 3 deletions cmd/buf/internal/command/generate/generate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -322,9 +322,6 @@ inputs:
testRunTypeArgs(t, map[string][]byte{
filepath.Join("gen", "a", "v1", "a.top-level-type-names.yaml"): []byte(`messages:
- a.v1.Foo
`),
filepath.Join("gen", "b", "v1", "b.top-level-type-names.yaml"): []byte(`messages:
- b.v1.Baz
`),
},
"--template",
Expand Down
9 changes: 9 additions & 0 deletions cmd/buf/testdata/imports/success/samepkg/a.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
syntax = "proto3";

package pkg;

import "b.proto";

message A {
B b = 1;
}
5 changes: 5 additions & 0 deletions cmd/buf/testdata/imports/success/samepkg/b.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
syntax = "proto3";

package pkg;

message B {}
7 changes: 7 additions & 0 deletions cmd/buf/testdata/imports/success/sibling/a.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
syntax = "proto3";

import "b.proto";

message A {
B b = 1;
}
3 changes: 3 additions & 0 deletions cmd/buf/testdata/imports/success/sibling/b.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
syntax = "proto3";

message B {}
42 changes: 26 additions & 16 deletions private/buf/bufctl/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -1435,22 +1435,14 @@ func filterImage(
) (bufimage.Image, error) {
newImage := image
var err error
if functionOptions.imageExcludeImports {
newImage = bufimage.ImageWithoutImports(newImage)
}
includeTypes := functionOptions.imageIncludeTypes
excludeTypes := functionOptions.imageExcludeTypes
if len(includeTypes) > 0 || len(excludeTypes) > 0 {
newImage, err = bufimageutil.FilterImage(
newImage,
bufimageutil.WithIncludeTypes(includeTypes...),
bufimageutil.WithExcludeTypes(excludeTypes...),
bufimageutil.WithMutateInPlace(),
)
if err != nil {
return nil, err
}
}
// Filter by path before filtering by type.
// - For a workspace input, the target paths were applied when the workspace
// was read, so the type filter already runs after them.
// - For a non-workspace input, we need to apply the path filters first too to
// stay consistent.
// The order matters because the type filter marks files that contain no
// included type as imports, and the path filter resets the import flag of every
// file that matches --path.
if !imageCameFromAWorkspace {
if len(functionOptions.targetPaths) > 0 || len(functionOptions.targetExcludePaths) > 0 {
// bufimage expects normalized paths, so we need to normalize the paths
Expand All @@ -1474,6 +1466,24 @@ func filterImage(
}
}
}
includeTypes := functionOptions.imageIncludeTypes
excludeTypes := functionOptions.imageExcludeTypes
if len(includeTypes) > 0 || len(excludeTypes) > 0 {
newImage, err = bufimageutil.FilterImage(
newImage,
bufimageutil.WithIncludeTypes(includeTypes...),
bufimageutil.WithExcludeTypes(excludeTypes...),
bufimageutil.WithMutateInPlace(),
)
if err != nil {
return nil, err
}
}
// Exclude imports last because the type filter (--type) and the path filter (--path) above can
// keep or re-add files as imports.
if functionOptions.imageExcludeImports {
newImage = bufimage.ImageWithoutImports(newImage)
}
return newImage, nil
}

Expand Down
23 changes: 21 additions & 2 deletions private/bufpkg/bufimage/bufimageutil/bufimageutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ func WithExcludeKnownExtensions() ImageFilterOption {
// that allows a named included type to be in an imported file or module. Without this
// option, only types defined directly in the image to be filtered are allowed.
// Excluded types are always allowed to be in imported files or modules.
//
// A file that defines an included type, named directly or through its package,
// is marked as a non-import, since it was selected, even if it was an import
// before the type filter is applied.
func WithAllowIncludeOfImportedType() ImageFilterOption {
return func(opts *imageFilterOptions) {
opts.allowImportedTypes = true
Expand All @@ -121,6 +125,14 @@ func WithAllowIncludeOfImportedType() ImageFilterOption {
// "acme.foo", all of its sub-packages, and all of their types; "acme.Foo.**"
// matches the message "acme.Foo" and all of its nested types.
//
// When both `--path` and WithIncludeTypes are passed, without [WithAllowIncludeOfImportedType],
// the resulting image is the union of the paths intersecting the union of the types. In this case,
// the type filter marks a file as an import if it doesn't contain an included type, but does not
// set any file as a target.
//
// With [WithAllowIncludeOfImportedType] set, the type filter sets a file as a target iff it
// contains an included type, and as an import otherwise.
//
// If the type does not exist in the image, an error wrapping
// [ErrImageFilterTypeNotFound] will be returned.
func WithIncludeTypes(typeNames ...string) ImageFilterOption {
Expand Down Expand Up @@ -290,6 +302,10 @@ type transitiveClosure struct {
// The set of imports for each file. This allows for re-writing imports
// for files whose contents have been pruned.
imports map[string]map[string]struct{}
// The paths of files that contain an explicitly included type or package.
// Every other file kept in the closure is present only because an included
// type references it, and is marked as an import.
includedFiles map[string]struct{}
}

type closureInclusionMode int
Expand All @@ -315,8 +331,9 @@ const (

func newTransitiveClosure() *transitiveClosure {
return &transitiveClosure{
elements: map[namedDescriptor]closureInclusionMode{},
imports: map[string]map[string]struct{}{},
elements: map[namedDescriptor]closureInclusionMode{},
imports: map[string]map[string]struct{}{},
includedFiles: map[string]struct{}{},
}
}

Expand Down Expand Up @@ -380,6 +397,7 @@ func (t *transitiveClosure) includeType(
if mode := t.elements[descriptorInfo.element]; mode == inclusionModeExcluded {
return fmt.Errorf("inclusion of type %q: %w", typeName, ErrImageFilterTypeExcluded)
}
t.includedFiles[descriptorInfo.file.Path()] = struct{}{}
// If an extension field, check if the extendee is excluded.
if field, ok := descriptorInfo.element.(*descriptorpb.FieldDescriptorProto); ok && field.Extendee != nil {
extendeeName := protoreflect.FullName(strings.TrimPrefix(field.GetExtendee(), "."))
Expand Down Expand Up @@ -467,6 +485,7 @@ func (t *transitiveClosure) includePackage(
if err := t.addElement(fileDescriptor, "", false, imageIndex, options); err != nil {
return included, fmt.Errorf("inclusion of package %q: %w", pkg.fullName, err)
}
t.includedFiles[file.Path()] = struct{}{}
included++
}
if recursive {
Expand Down
Loading
Loading