diff --git a/BUILD.bazel b/BUILD.bazel index 2e18d681..dc16402e 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -130,6 +130,7 @@ package_filegroup( "//pkg/scalarule/mocks:filegroup", "//pkg/semanticdb:filegroup", "//pkg/starlarkeval:filegroup", + "//pkg/sweep:filegroup", "//pkg/testutil:filegroup", "//pkg/wildcardimport:filegroup", "//rules:filegroup", diff --git a/build/stack/gazelle/scala/autokeep/autokeep.pb.go b/build/stack/gazelle/scala/autokeep/autokeep.pb.go index 0ffc597c..652396df 100644 --- a/build/stack/gazelle/scala/autokeep/autokeep.pb.go +++ b/build/stack/gazelle/scala/autokeep/autokeep.pb.go @@ -7,7 +7,6 @@ package autokeep import ( - _ "github.com/stackb/scala-gazelle/build/stack/gazelle/scala/parse" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" @@ -79,6 +78,7 @@ type ScalacError struct { // *ScalacError_MissingSymbol // *ScalacError_NotAMemberOfPackage // *ScalacError_BuildozerUnusedDep + // *ScalacError_NotFound Error isScalacError_Error `protobuf_oneof:"error"` } @@ -156,6 +156,13 @@ func (x *ScalacError) GetBuildozerUnusedDep() *BuildozerUnusedDep { return nil } +func (x *ScalacError) GetNotFound() *TypeNotFound { + if x, ok := x.GetError().(*ScalacError_NotFound); ok { + return x.NotFound + } + return nil +} + type isScalacError_Error interface { isScalacError_Error() } @@ -172,12 +179,18 @@ type ScalacError_BuildozerUnusedDep struct { BuildozerUnusedDep *BuildozerUnusedDep `protobuf:"bytes,5,opt,name=buildozer_unused_dep,json=buildozerUnusedDep,proto3,oneof"` } +type ScalacError_NotFound struct { + NotFound *TypeNotFound `protobuf:"bytes,6,opt,name=not_found,json=notFound,proto3,oneof"` +} + func (*ScalacError_MissingSymbol) isScalacError_Error() {} func (*ScalacError_NotAMemberOfPackage) isScalacError_Error() {} func (*ScalacError_BuildozerUnusedDep) isScalacError_Error() {} +func (*ScalacError_NotFound) isScalacError_Error() {} + type MissingSymbol struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -246,8 +259,9 @@ type NotAMemberOfPackage struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - PackageName string `protobuf:"bytes,1,opt,name=package_name,json=packageName,proto3" json:"package_name,omitempty"` - Symbol string `protobuf:"bytes,2,opt,name=symbol,proto3" json:"symbol,omitempty"` + SourceFile string `protobuf:"bytes,1,opt,name=source_file,json=sourceFile,proto3" json:"source_file,omitempty"` + PackageName string `protobuf:"bytes,2,opt,name=package_name,json=packageName,proto3" json:"package_name,omitempty"` + Symbol string `protobuf:"bytes,3,opt,name=symbol,proto3" json:"symbol,omitempty"` } func (x *NotAMemberOfPackage) Reset() { @@ -282,6 +296,13 @@ func (*NotAMemberOfPackage) Descriptor() ([]byte, []int) { return file_build_stack_gazelle_scala_autokeep_autokeep_proto_rawDescGZIP(), []int{3} } +func (x *NotAMemberOfPackage) GetSourceFile() string { + if x != nil { + return x.SourceFile + } + return "" +} + func (x *NotAMemberOfPackage) GetPackageName() string { if x != nil { return x.PackageName @@ -351,6 +372,61 @@ func (x *BuildozerUnusedDep) GetUnusedDep() string { return "" } +type TypeNotFound struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + SourceFile string `protobuf:"bytes,1,opt,name=source_file,json=sourceFile,proto3" json:"source_file,omitempty"` + Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"` +} + +func (x *TypeNotFound) Reset() { + *x = TypeNotFound{} + if protoimpl.UnsafeEnabled { + mi := &file_build_stack_gazelle_scala_autokeep_autokeep_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TypeNotFound) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TypeNotFound) ProtoMessage() {} + +func (x *TypeNotFound) ProtoReflect() protoreflect.Message { + mi := &file_build_stack_gazelle_scala_autokeep_autokeep_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TypeNotFound.ProtoReflect.Descriptor instead. +func (*TypeNotFound) Descriptor() ([]byte, []int) { + return file_build_stack_gazelle_scala_autokeep_autokeep_proto_rawDescGZIP(), []int{5} +} + +func (x *TypeNotFound) GetSourceFile() string { + if x != nil { + return x.SourceFile + } + return "" +} + +func (x *TypeNotFound) GetType() string { + if x != nil { + return x.Type + } + return "" +} + type RuleDeps struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -364,7 +440,7 @@ type RuleDeps struct { func (x *RuleDeps) Reset() { *x = RuleDeps{} if protoimpl.UnsafeEnabled { - mi := &file_build_stack_gazelle_scala_autokeep_autokeep_proto_msgTypes[5] + mi := &file_build_stack_gazelle_scala_autokeep_autokeep_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -377,7 +453,7 @@ func (x *RuleDeps) String() string { func (*RuleDeps) ProtoMessage() {} func (x *RuleDeps) ProtoReflect() protoreflect.Message { - mi := &file_build_stack_gazelle_scala_autokeep_autokeep_proto_msgTypes[5] + mi := &file_build_stack_gazelle_scala_autokeep_autokeep_proto_msgTypes[6] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -390,7 +466,7 @@ func (x *RuleDeps) ProtoReflect() protoreflect.Message { // Deprecated: Use RuleDeps.ProtoReflect.Descriptor instead. func (*RuleDeps) Descriptor() ([]byte, []int) { - return file_build_stack_gazelle_scala_autokeep_autokeep_proto_rawDescGZIP(), []int{5} + return file_build_stack_gazelle_scala_autokeep_autokeep_proto_rawDescGZIP(), []int{6} } func (x *RuleDeps) GetLabel() string { @@ -426,7 +502,7 @@ type DeltaDeps struct { func (x *DeltaDeps) Reset() { *x = DeltaDeps{} if protoimpl.UnsafeEnabled { - mi := &file_build_stack_gazelle_scala_autokeep_autokeep_proto_msgTypes[6] + mi := &file_build_stack_gazelle_scala_autokeep_autokeep_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -439,7 +515,7 @@ func (x *DeltaDeps) String() string { func (*DeltaDeps) ProtoMessage() {} func (x *DeltaDeps) ProtoReflect() protoreflect.Message { - mi := &file_build_stack_gazelle_scala_autokeep_autokeep_proto_msgTypes[6] + mi := &file_build_stack_gazelle_scala_autokeep_autokeep_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -452,7 +528,7 @@ func (x *DeltaDeps) ProtoReflect() protoreflect.Message { // Deprecated: Use DeltaDeps.ProtoReflect.Descriptor instead. func (*DeltaDeps) Descriptor() ([]byte, []int) { - return file_build_stack_gazelle_scala_autokeep_autokeep_proto_rawDescGZIP(), []int{6} + return file_build_stack_gazelle_scala_autokeep_autokeep_proto_rawDescGZIP(), []int{7} } func (x *DeltaDeps) GetAdd() []*RuleDeps { @@ -477,80 +553,89 @@ var file_build_stack_gazelle_scala_autokeep_autokeep_proto_rawDesc = []byte{ 0x6b, 0x65, 0x65, 0x70, 0x2f, 0x61, 0x75, 0x74, 0x6f, 0x6b, 0x65, 0x65, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x22, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x2e, 0x67, 0x61, 0x7a, 0x65, 0x6c, 0x6c, 0x65, 0x2e, 0x73, 0x63, 0x61, 0x6c, 0x61, 0x2e, 0x61, - 0x75, 0x74, 0x6f, 0x6b, 0x65, 0x65, 0x70, 0x1a, 0x2a, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x2f, 0x73, - 0x74, 0x61, 0x63, 0x6b, 0x2f, 0x67, 0x61, 0x7a, 0x65, 0x6c, 0x6c, 0x65, 0x2f, 0x73, 0x63, 0x61, - 0x6c, 0x61, 0x2f, 0x70, 0x61, 0x72, 0x73, 0x65, 0x2f, 0x72, 0x75, 0x6c, 0x65, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x22, 0x63, 0x0a, 0x0b, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, - 0x63, 0x73, 0x12, 0x54, 0x0a, 0x0d, 0x73, 0x63, 0x61, 0x6c, 0x61, 0x63, 0x5f, 0x65, 0x72, 0x72, - 0x6f, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x62, 0x75, 0x69, 0x6c, - 0x64, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x2e, 0x67, 0x61, 0x7a, 0x65, 0x6c, 0x6c, 0x65, 0x2e, - 0x73, 0x63, 0x61, 0x6c, 0x61, 0x2e, 0x61, 0x75, 0x74, 0x6f, 0x6b, 0x65, 0x65, 0x70, 0x2e, 0x53, - 0x63, 0x61, 0x6c, 0x61, 0x63, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x0c, 0x73, 0x63, 0x61, 0x6c, - 0x61, 0x63, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x22, 0x8d, 0x03, 0x0a, 0x0b, 0x53, 0x63, 0x61, - 0x6c, 0x61, 0x63, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x75, 0x6c, 0x65, - 0x5f, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x75, - 0x6c, 0x65, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x12, 0x1d, 0x0a, 0x0a, 0x62, 0x75, 0x69, 0x6c, 0x64, - 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x62, 0x75, 0x69, - 0x6c, 0x64, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x5a, 0x0a, 0x0e, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6e, - 0x67, 0x5f, 0x73, 0x79, 0x6d, 0x62, 0x6f, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x31, - 0x2e, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x2e, 0x67, 0x61, 0x7a, - 0x65, 0x6c, 0x6c, 0x65, 0x2e, 0x73, 0x63, 0x61, 0x6c, 0x61, 0x2e, 0x61, 0x75, 0x74, 0x6f, 0x6b, - 0x65, 0x65, 0x70, 0x2e, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6e, 0x67, 0x53, 0x79, 0x6d, 0x62, 0x6f, - 0x6c, 0x48, 0x00, 0x52, 0x0d, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6e, 0x67, 0x53, 0x79, 0x6d, 0x62, - 0x6f, 0x6c, 0x12, 0x6f, 0x0a, 0x17, 0x6e, 0x6f, 0x74, 0x5f, 0x61, 0x5f, 0x6d, 0x65, 0x6d, 0x62, - 0x65, 0x72, 0x5f, 0x6f, 0x66, 0x5f, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x37, 0x2e, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x2e, 0x73, 0x74, 0x61, 0x63, - 0x6b, 0x2e, 0x67, 0x61, 0x7a, 0x65, 0x6c, 0x6c, 0x65, 0x2e, 0x73, 0x63, 0x61, 0x6c, 0x61, 0x2e, - 0x61, 0x75, 0x74, 0x6f, 0x6b, 0x65, 0x65, 0x70, 0x2e, 0x4e, 0x6f, 0x74, 0x41, 0x4d, 0x65, 0x6d, - 0x62, 0x65, 0x72, 0x4f, 0x66, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x48, 0x00, 0x52, 0x13, - 0x6e, 0x6f, 0x74, 0x41, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x4f, 0x66, 0x50, 0x61, 0x63, 0x6b, - 0x61, 0x67, 0x65, 0x12, 0x6a, 0x0a, 0x14, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x6f, 0x7a, 0x65, 0x72, - 0x5f, 0x75, 0x6e, 0x75, 0x73, 0x65, 0x64, 0x5f, 0x64, 0x65, 0x70, 0x18, 0x05, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x36, 0x2e, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x2e, - 0x67, 0x61, 0x7a, 0x65, 0x6c, 0x6c, 0x65, 0x2e, 0x73, 0x63, 0x61, 0x6c, 0x61, 0x2e, 0x61, 0x75, - 0x74, 0x6f, 0x6b, 0x65, 0x65, 0x70, 0x2e, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x6f, 0x7a, 0x65, 0x72, - 0x55, 0x6e, 0x75, 0x73, 0x65, 0x64, 0x44, 0x65, 0x70, 0x48, 0x00, 0x52, 0x12, 0x62, 0x75, 0x69, - 0x6c, 0x64, 0x6f, 0x7a, 0x65, 0x72, 0x55, 0x6e, 0x75, 0x73, 0x65, 0x64, 0x44, 0x65, 0x70, 0x42, - 0x07, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x69, 0x0a, 0x0d, 0x4d, 0x69, 0x73, 0x73, - 0x69, 0x6e, 0x67, 0x53, 0x79, 0x6d, 0x62, 0x6f, 0x6c, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x79, - 0x6d, 0x62, 0x6f, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x79, 0x6d, 0x62, - 0x6f, 0x6c, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x5f, 0x62, - 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, - 0x64, 0x42, 0x79, 0x22, 0x50, 0x0a, 0x13, 0x4e, 0x6f, 0x74, 0x41, 0x4d, 0x65, 0x6d, 0x62, 0x65, - 0x72, 0x4f, 0x66, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x70, 0x61, - 0x63, 0x6b, 0x61, 0x67, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0b, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, - 0x06, 0x73, 0x79, 0x6d, 0x62, 0x6f, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, - 0x79, 0x6d, 0x62, 0x6f, 0x6c, 0x22, 0x52, 0x0a, 0x12, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x6f, 0x7a, - 0x65, 0x72, 0x55, 0x6e, 0x75, 0x73, 0x65, 0x64, 0x44, 0x65, 0x70, 0x12, 0x1d, 0x0a, 0x0a, 0x72, - 0x75, 0x6c, 0x65, 0x5f, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x09, 0x72, 0x75, 0x6c, 0x65, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x12, 0x1d, 0x0a, 0x0a, 0x75, 0x6e, - 0x75, 0x73, 0x65, 0x64, 0x5f, 0x64, 0x65, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, - 0x75, 0x6e, 0x75, 0x73, 0x65, 0x64, 0x44, 0x65, 0x70, 0x22, 0x53, 0x0a, 0x08, 0x52, 0x75, 0x6c, - 0x65, 0x44, 0x65, 0x70, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x12, 0x1d, 0x0a, 0x0a, 0x62, + 0x75, 0x74, 0x6f, 0x6b, 0x65, 0x65, 0x70, 0x22, 0x63, 0x0a, 0x0b, 0x44, 0x69, 0x61, 0x67, 0x6e, + 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x12, 0x54, 0x0a, 0x0d, 0x73, 0x63, 0x61, 0x6c, 0x61, 0x63, + 0x5f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2f, 0x2e, + 0x62, 0x75, 0x69, 0x6c, 0x64, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x2e, 0x67, 0x61, 0x7a, 0x65, + 0x6c, 0x6c, 0x65, 0x2e, 0x73, 0x63, 0x61, 0x6c, 0x61, 0x2e, 0x61, 0x75, 0x74, 0x6f, 0x6b, 0x65, + 0x65, 0x70, 0x2e, 0x53, 0x63, 0x61, 0x6c, 0x61, 0x63, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x0c, + 0x73, 0x63, 0x61, 0x6c, 0x61, 0x63, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x22, 0xde, 0x03, 0x0a, + 0x0b, 0x53, 0x63, 0x61, 0x6c, 0x61, 0x63, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x1d, 0x0a, 0x0a, + 0x72, 0x75, 0x6c, 0x65, 0x5f, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x09, 0x72, 0x75, 0x6c, 0x65, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x12, 0x1d, 0x0a, 0x0a, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x09, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x65, - 0x70, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x64, 0x65, 0x70, 0x73, 0x22, 0x91, - 0x01, 0x0a, 0x09, 0x44, 0x65, 0x6c, 0x74, 0x61, 0x44, 0x65, 0x70, 0x73, 0x12, 0x3e, 0x0a, 0x03, - 0x61, 0x64, 0x64, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x62, 0x75, 0x69, 0x6c, - 0x64, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x2e, 0x67, 0x61, 0x7a, 0x65, 0x6c, 0x6c, 0x65, 0x2e, - 0x73, 0x63, 0x61, 0x6c, 0x61, 0x2e, 0x61, 0x75, 0x74, 0x6f, 0x6b, 0x65, 0x65, 0x70, 0x2e, 0x52, - 0x75, 0x6c, 0x65, 0x44, 0x65, 0x70, 0x73, 0x52, 0x03, 0x61, 0x64, 0x64, 0x12, 0x44, 0x0a, 0x06, - 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x62, - 0x75, 0x69, 0x6c, 0x64, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x2e, 0x67, 0x61, 0x7a, 0x65, 0x6c, - 0x6c, 0x65, 0x2e, 0x73, 0x63, 0x61, 0x6c, 0x61, 0x2e, 0x61, 0x75, 0x74, 0x6f, 0x6b, 0x65, 0x65, - 0x70, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x44, 0x65, 0x70, 0x73, 0x52, 0x06, 0x72, 0x65, 0x6d, 0x6f, - 0x76, 0x65, 0x42, 0x73, 0x0a, 0x22, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x2e, 0x73, 0x74, 0x61, 0x63, - 0x6b, 0x2e, 0x67, 0x61, 0x7a, 0x65, 0x6c, 0x6c, 0x65, 0x2e, 0x73, 0x63, 0x61, 0x6c, 0x61, 0x2e, - 0x61, 0x75, 0x74, 0x6f, 0x6b, 0x65, 0x65, 0x70, 0x50, 0x01, 0x5a, 0x4b, 0x67, 0x69, 0x74, 0x68, - 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x62, 0x2f, 0x73, 0x63, - 0x61, 0x6c, 0x61, 0x2d, 0x67, 0x61, 0x7a, 0x65, 0x6c, 0x6c, 0x65, 0x2f, 0x62, 0x75, 0x69, 0x6c, - 0x64, 0x2f, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x2f, 0x67, 0x61, 0x7a, 0x65, 0x6c, 0x6c, 0x65, 0x2f, - 0x73, 0x63, 0x61, 0x6c, 0x61, 0x2f, 0x61, 0x75, 0x74, 0x6f, 0x6b, 0x65, 0x65, 0x70, 0x3b, 0x61, - 0x75, 0x74, 0x6f, 0x6b, 0x65, 0x65, 0x70, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x09, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x5a, 0x0a, 0x0e, 0x6d, 0x69, + 0x73, 0x73, 0x69, 0x6e, 0x67, 0x5f, 0x73, 0x79, 0x6d, 0x62, 0x6f, 0x6c, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x31, 0x2e, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, + 0x2e, 0x67, 0x61, 0x7a, 0x65, 0x6c, 0x6c, 0x65, 0x2e, 0x73, 0x63, 0x61, 0x6c, 0x61, 0x2e, 0x61, + 0x75, 0x74, 0x6f, 0x6b, 0x65, 0x65, 0x70, 0x2e, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6e, 0x67, 0x53, + 0x79, 0x6d, 0x62, 0x6f, 0x6c, 0x48, 0x00, 0x52, 0x0d, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6e, 0x67, + 0x53, 0x79, 0x6d, 0x62, 0x6f, 0x6c, 0x12, 0x6f, 0x0a, 0x17, 0x6e, 0x6f, 0x74, 0x5f, 0x61, 0x5f, + 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x5f, 0x6f, 0x66, 0x5f, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, + 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x37, 0x2e, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x2e, + 0x73, 0x74, 0x61, 0x63, 0x6b, 0x2e, 0x67, 0x61, 0x7a, 0x65, 0x6c, 0x6c, 0x65, 0x2e, 0x73, 0x63, + 0x61, 0x6c, 0x61, 0x2e, 0x61, 0x75, 0x74, 0x6f, 0x6b, 0x65, 0x65, 0x70, 0x2e, 0x4e, 0x6f, 0x74, + 0x41, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x4f, 0x66, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, + 0x48, 0x00, 0x52, 0x13, 0x6e, 0x6f, 0x74, 0x41, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x4f, 0x66, + 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x12, 0x6a, 0x0a, 0x14, 0x62, 0x75, 0x69, 0x6c, 0x64, + 0x6f, 0x7a, 0x65, 0x72, 0x5f, 0x75, 0x6e, 0x75, 0x73, 0x65, 0x64, 0x5f, 0x64, 0x65, 0x70, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x36, 0x2e, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x2e, 0x73, 0x74, + 0x61, 0x63, 0x6b, 0x2e, 0x67, 0x61, 0x7a, 0x65, 0x6c, 0x6c, 0x65, 0x2e, 0x73, 0x63, 0x61, 0x6c, + 0x61, 0x2e, 0x61, 0x75, 0x74, 0x6f, 0x6b, 0x65, 0x65, 0x70, 0x2e, 0x42, 0x75, 0x69, 0x6c, 0x64, + 0x6f, 0x7a, 0x65, 0x72, 0x55, 0x6e, 0x75, 0x73, 0x65, 0x64, 0x44, 0x65, 0x70, 0x48, 0x00, 0x52, + 0x12, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x6f, 0x7a, 0x65, 0x72, 0x55, 0x6e, 0x75, 0x73, 0x65, 0x64, + 0x44, 0x65, 0x70, 0x12, 0x4f, 0x0a, 0x09, 0x6e, 0x6f, 0x74, 0x5f, 0x66, 0x6f, 0x75, 0x6e, 0x64, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x2e, 0x73, + 0x74, 0x61, 0x63, 0x6b, 0x2e, 0x67, 0x61, 0x7a, 0x65, 0x6c, 0x6c, 0x65, 0x2e, 0x73, 0x63, 0x61, + 0x6c, 0x61, 0x2e, 0x61, 0x75, 0x74, 0x6f, 0x6b, 0x65, 0x65, 0x70, 0x2e, 0x54, 0x79, 0x70, 0x65, + 0x4e, 0x6f, 0x74, 0x46, 0x6f, 0x75, 0x6e, 0x64, 0x48, 0x00, 0x52, 0x08, 0x6e, 0x6f, 0x74, 0x46, + 0x6f, 0x75, 0x6e, 0x64, 0x42, 0x07, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x69, 0x0a, + 0x0d, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6e, 0x67, 0x53, 0x79, 0x6d, 0x62, 0x6f, 0x6c, 0x12, 0x1f, + 0x0a, 0x0b, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x46, 0x69, 0x6c, 0x65, 0x12, + 0x16, 0x0a, 0x06, 0x73, 0x79, 0x6d, 0x62, 0x6f, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x06, 0x73, 0x79, 0x6d, 0x62, 0x6f, 0x6c, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x71, 0x75, 0x69, + 0x72, 0x65, 0x64, 0x5f, 0x62, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x65, + 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x42, 0x79, 0x22, 0x71, 0x0a, 0x13, 0x4e, 0x6f, 0x74, 0x41, + 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x4f, 0x66, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x12, + 0x1f, 0x0a, 0x0b, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x46, 0x69, 0x6c, 0x65, + 0x12, 0x21, 0x0a, 0x0c, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x4e, + 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x79, 0x6d, 0x62, 0x6f, 0x6c, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x79, 0x6d, 0x62, 0x6f, 0x6c, 0x22, 0x52, 0x0a, 0x12, 0x42, + 0x75, 0x69, 0x6c, 0x64, 0x6f, 0x7a, 0x65, 0x72, 0x55, 0x6e, 0x75, 0x73, 0x65, 0x64, 0x44, 0x65, + 0x70, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x75, 0x6c, 0x65, 0x5f, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x75, 0x6c, 0x65, 0x4c, 0x61, 0x62, 0x65, 0x6c, + 0x12, 0x1d, 0x0a, 0x0a, 0x75, 0x6e, 0x75, 0x73, 0x65, 0x64, 0x5f, 0x64, 0x65, 0x70, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x75, 0x6e, 0x75, 0x73, 0x65, 0x64, 0x44, 0x65, 0x70, 0x22, + 0x43, 0x0a, 0x0c, 0x54, 0x79, 0x70, 0x65, 0x4e, 0x6f, 0x74, 0x46, 0x6f, 0x75, 0x6e, 0x64, 0x12, + 0x1f, 0x0a, 0x0b, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x46, 0x69, 0x6c, 0x65, + 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, + 0x74, 0x79, 0x70, 0x65, 0x22, 0x53, 0x0a, 0x08, 0x52, 0x75, 0x6c, 0x65, 0x44, 0x65, 0x70, 0x73, + 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x12, 0x1d, 0x0a, 0x0a, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, + 0x66, 0x69, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x62, 0x75, 0x69, 0x6c, + 0x64, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x65, 0x70, 0x73, 0x18, 0x03, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x04, 0x64, 0x65, 0x70, 0x73, 0x22, 0x91, 0x01, 0x0a, 0x09, 0x44, 0x65, + 0x6c, 0x74, 0x61, 0x44, 0x65, 0x70, 0x73, 0x12, 0x3e, 0x0a, 0x03, 0x61, 0x64, 0x64, 0x18, 0x01, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x2e, 0x73, 0x74, 0x61, + 0x63, 0x6b, 0x2e, 0x67, 0x61, 0x7a, 0x65, 0x6c, 0x6c, 0x65, 0x2e, 0x73, 0x63, 0x61, 0x6c, 0x61, + 0x2e, 0x61, 0x75, 0x74, 0x6f, 0x6b, 0x65, 0x65, 0x70, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x44, 0x65, + 0x70, 0x73, 0x52, 0x03, 0x61, 0x64, 0x64, 0x12, 0x44, 0x0a, 0x06, 0x72, 0x65, 0x6d, 0x6f, 0x76, + 0x65, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x2e, + 0x73, 0x74, 0x61, 0x63, 0x6b, 0x2e, 0x67, 0x61, 0x7a, 0x65, 0x6c, 0x6c, 0x65, 0x2e, 0x73, 0x63, + 0x61, 0x6c, 0x61, 0x2e, 0x61, 0x75, 0x74, 0x6f, 0x6b, 0x65, 0x65, 0x70, 0x2e, 0x52, 0x75, 0x6c, + 0x65, 0x44, 0x65, 0x70, 0x73, 0x52, 0x06, 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x42, 0x73, 0x0a, + 0x22, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x2e, 0x67, 0x61, 0x7a, + 0x65, 0x6c, 0x6c, 0x65, 0x2e, 0x73, 0x63, 0x61, 0x6c, 0x61, 0x2e, 0x61, 0x75, 0x74, 0x6f, 0x6b, + 0x65, 0x65, 0x70, 0x50, 0x01, 0x5a, 0x4b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, + 0x6d, 0x2f, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x62, 0x2f, 0x73, 0x63, 0x61, 0x6c, 0x61, 0x2d, 0x67, + 0x61, 0x7a, 0x65, 0x6c, 0x6c, 0x65, 0x2f, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x2f, 0x73, 0x74, 0x61, + 0x63, 0x6b, 0x2f, 0x67, 0x61, 0x7a, 0x65, 0x6c, 0x6c, 0x65, 0x2f, 0x73, 0x63, 0x61, 0x6c, 0x61, + 0x2f, 0x61, 0x75, 0x74, 0x6f, 0x6b, 0x65, 0x65, 0x70, 0x3b, 0x61, 0x75, 0x74, 0x6f, 0x6b, 0x65, + 0x65, 0x70, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -565,28 +650,30 @@ func file_build_stack_gazelle_scala_autokeep_autokeep_proto_rawDescGZIP() []byte return file_build_stack_gazelle_scala_autokeep_autokeep_proto_rawDescData } -var file_build_stack_gazelle_scala_autokeep_autokeep_proto_msgTypes = make([]protoimpl.MessageInfo, 7) +var file_build_stack_gazelle_scala_autokeep_autokeep_proto_msgTypes = make([]protoimpl.MessageInfo, 8) var file_build_stack_gazelle_scala_autokeep_autokeep_proto_goTypes = []interface{}{ (*Diagnostics)(nil), // 0: build.stack.gazelle.scala.autokeep.Diagnostics (*ScalacError)(nil), // 1: build.stack.gazelle.scala.autokeep.ScalacError (*MissingSymbol)(nil), // 2: build.stack.gazelle.scala.autokeep.MissingSymbol (*NotAMemberOfPackage)(nil), // 3: build.stack.gazelle.scala.autokeep.NotAMemberOfPackage (*BuildozerUnusedDep)(nil), // 4: build.stack.gazelle.scala.autokeep.BuildozerUnusedDep - (*RuleDeps)(nil), // 5: build.stack.gazelle.scala.autokeep.RuleDeps - (*DeltaDeps)(nil), // 6: build.stack.gazelle.scala.autokeep.DeltaDeps + (*TypeNotFound)(nil), // 5: build.stack.gazelle.scala.autokeep.TypeNotFound + (*RuleDeps)(nil), // 6: build.stack.gazelle.scala.autokeep.RuleDeps + (*DeltaDeps)(nil), // 7: build.stack.gazelle.scala.autokeep.DeltaDeps } var file_build_stack_gazelle_scala_autokeep_autokeep_proto_depIdxs = []int32{ 1, // 0: build.stack.gazelle.scala.autokeep.Diagnostics.scalac_errors:type_name -> build.stack.gazelle.scala.autokeep.ScalacError 2, // 1: build.stack.gazelle.scala.autokeep.ScalacError.missing_symbol:type_name -> build.stack.gazelle.scala.autokeep.MissingSymbol 3, // 2: build.stack.gazelle.scala.autokeep.ScalacError.not_a_member_of_package:type_name -> build.stack.gazelle.scala.autokeep.NotAMemberOfPackage 4, // 3: build.stack.gazelle.scala.autokeep.ScalacError.buildozer_unused_dep:type_name -> build.stack.gazelle.scala.autokeep.BuildozerUnusedDep - 5, // 4: build.stack.gazelle.scala.autokeep.DeltaDeps.add:type_name -> build.stack.gazelle.scala.autokeep.RuleDeps - 5, // 5: build.stack.gazelle.scala.autokeep.DeltaDeps.remove:type_name -> build.stack.gazelle.scala.autokeep.RuleDeps - 6, // [6:6] is the sub-list for method output_type - 6, // [6:6] is the sub-list for method input_type - 6, // [6:6] is the sub-list for extension type_name - 6, // [6:6] is the sub-list for extension extendee - 0, // [0:6] is the sub-list for field type_name + 5, // 4: build.stack.gazelle.scala.autokeep.ScalacError.not_found:type_name -> build.stack.gazelle.scala.autokeep.TypeNotFound + 6, // 5: build.stack.gazelle.scala.autokeep.DeltaDeps.add:type_name -> build.stack.gazelle.scala.autokeep.RuleDeps + 6, // 6: build.stack.gazelle.scala.autokeep.DeltaDeps.remove:type_name -> build.stack.gazelle.scala.autokeep.RuleDeps + 7, // [7:7] is the sub-list for method output_type + 7, // [7:7] is the sub-list for method input_type + 7, // [7:7] is the sub-list for extension type_name + 7, // [7:7] is the sub-list for extension extendee + 0, // [0:7] is the sub-list for field type_name } func init() { file_build_stack_gazelle_scala_autokeep_autokeep_proto_init() } @@ -656,7 +743,7 @@ func file_build_stack_gazelle_scala_autokeep_autokeep_proto_init() { } } file_build_stack_gazelle_scala_autokeep_autokeep_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RuleDeps); i { + switch v := v.(*TypeNotFound); i { case 0: return &v.state case 1: @@ -668,6 +755,18 @@ func file_build_stack_gazelle_scala_autokeep_autokeep_proto_init() { } } file_build_stack_gazelle_scala_autokeep_autokeep_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RuleDeps); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_build_stack_gazelle_scala_autokeep_autokeep_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*DeltaDeps); i { case 0: return &v.state @@ -684,6 +783,7 @@ func file_build_stack_gazelle_scala_autokeep_autokeep_proto_init() { (*ScalacError_MissingSymbol)(nil), (*ScalacError_NotAMemberOfPackage)(nil), (*ScalacError_BuildozerUnusedDep)(nil), + (*ScalacError_NotFound)(nil), } type x struct{} out := protoimpl.TypeBuilder{ @@ -691,7 +791,7 @@ func file_build_stack_gazelle_scala_autokeep_autokeep_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_build_stack_gazelle_scala_autokeep_autokeep_proto_rawDesc, NumEnums: 0, - NumMessages: 7, + NumMessages: 8, NumExtensions: 0, NumServices: 0, }, diff --git a/build/stack/gazelle/scala/autokeep/autokeep.proto b/build/stack/gazelle/scala/autokeep/autokeep.proto index 3bcdd50c..51a87e37 100644 --- a/build/stack/gazelle/scala/autokeep/autokeep.proto +++ b/build/stack/gazelle/scala/autokeep/autokeep.proto @@ -2,8 +2,6 @@ syntax = "proto3"; package build.stack.gazelle.scala.autokeep; -import "build/stack/gazelle/scala/parse/rule.proto"; - option go_package = "github.com/stackb/scala-gazelle/build/stack/gazelle/scala/autokeep;autokeep"; option java_package = "build.stack.gazelle.scala.autokeep"; option java_multiple_files = true; @@ -19,6 +17,7 @@ message ScalacError { MissingSymbol missing_symbol = 3; NotAMemberOfPackage not_a_member_of_package = 4; BuildozerUnusedDep buildozer_unused_dep = 5; + TypeNotFound not_found = 6; } } @@ -29,8 +28,9 @@ message MissingSymbol { } message NotAMemberOfPackage { - string package_name = 1; - string symbol = 2; + string source_file = 1; + string package_name = 2; + string symbol = 3; } message BuildozerUnusedDep { @@ -38,6 +38,11 @@ message BuildozerUnusedDep { string unused_dep = 2; } +message TypeNotFound { + string source_file = 1; + string type = 2; +} + message RuleDeps { string label = 1; string build_file = 2; diff --git a/build/stack/gazelle/scala/parse/file.pb.go b/build/stack/gazelle/scala/parse/file.pb.go index 934f126e..4ed09fd1 100644 --- a/build/stack/gazelle/scala/parse/file.pb.go +++ b/build/stack/gazelle/scala/parse/file.pb.go @@ -85,6 +85,7 @@ type File struct { Extends map[string]*ClassList `protobuf:"bytes,11,rep,name=extends,proto3" json:"extends,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` Error string `protobuf:"bytes,13,opt,name=error,proto3" json:"error,omitempty"` Tree string `protobuf:"bytes,14,opt,name=tree,proto3" json:"tree,omitempty"` + Sha256 string `protobuf:"bytes,15,opt,name=sha256,proto3" json:"sha256,omitempty"` } func (x *File) Reset() { @@ -210,6 +211,13 @@ func (x *File) GetTree() string { return "" } +func (x *File) GetSha256() string { + if x != nil { + return x.Sha256 + } + return "" +} + type ClassList struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -269,7 +277,7 @@ var file_build_stack_gazelle_scala_parse_file_proto_rawDesc = []byte{ 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x2e, 0x67, 0x61, 0x7a, 0x65, 0x6c, 0x6c, 0x65, 0x2e, 0x73, 0x63, 0x61, 0x6c, 0x61, 0x2e, 0x70, 0x61, 0x72, 0x73, 0x65, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x05, - 0x66, 0x69, 0x6c, 0x65, 0x73, 0x22, 0xef, 0x03, 0x0a, 0x04, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x1a, + 0x66, 0x69, 0x6c, 0x65, 0x73, 0x22, 0x87, 0x04, 0x0a, 0x04, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x29, 0x0a, 0x10, 0x73, 0x65, 0x6d, 0x61, 0x6e, 0x74, 0x69, 0x63, 0x5f, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x18, 0x02, @@ -293,24 +301,25 @@ var file_build_stack_gazelle_scala_parse_file_proto_rawDesc = []byte{ 0x45, 0x78, 0x74, 0x65, 0x6e, 0x64, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x64, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x74, - 0x72, 0x65, 0x65, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x72, 0x65, 0x65, 0x1a, - 0x66, 0x0a, 0x0c, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x64, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, - 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, - 0x79, 0x12, 0x40, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x2a, 0x2e, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x2e, 0x67, - 0x61, 0x7a, 0x65, 0x6c, 0x6c, 0x65, 0x2e, 0x73, 0x63, 0x61, 0x6c, 0x61, 0x2e, 0x70, 0x61, 0x72, - 0x73, 0x65, 0x2e, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x05, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x25, 0x0a, 0x09, 0x43, 0x6c, 0x61, 0x73, 0x73, - 0x4c, 0x69, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x65, 0x73, 0x18, - 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x65, 0x73, 0x42, 0x6a, - 0x0a, 0x1f, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x2e, 0x67, 0x61, - 0x7a, 0x65, 0x6c, 0x6c, 0x65, 0x2e, 0x73, 0x63, 0x61, 0x6c, 0x61, 0x2e, 0x70, 0x61, 0x72, 0x73, - 0x65, 0x50, 0x01, 0x5a, 0x45, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, - 0x73, 0x74, 0x61, 0x63, 0x6b, 0x62, 0x2f, 0x73, 0x63, 0x61, 0x6c, 0x61, 0x2d, 0x67, 0x61, 0x7a, - 0x65, 0x6c, 0x6c, 0x65, 0x2f, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x2f, 0x73, 0x74, 0x61, 0x63, 0x6b, - 0x2f, 0x67, 0x61, 0x7a, 0x65, 0x6c, 0x6c, 0x65, 0x2f, 0x73, 0x63, 0x61, 0x6c, 0x61, 0x2f, 0x70, - 0x61, 0x72, 0x73, 0x65, 0x3b, 0x70, 0x61, 0x72, 0x73, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x33, + 0x72, 0x65, 0x65, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x72, 0x65, 0x65, 0x12, + 0x16, 0x0a, 0x06, 0x73, 0x68, 0x61, 0x32, 0x35, 0x36, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x06, 0x73, 0x68, 0x61, 0x32, 0x35, 0x36, 0x1a, 0x66, 0x0a, 0x0c, 0x45, 0x78, 0x74, 0x65, 0x6e, + 0x64, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x40, 0x0a, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x62, 0x75, 0x69, 0x6c, 0x64, + 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x2e, 0x67, 0x61, 0x7a, 0x65, 0x6c, 0x6c, 0x65, 0x2e, 0x73, + 0x63, 0x61, 0x6c, 0x61, 0x2e, 0x70, 0x61, 0x72, 0x73, 0x65, 0x2e, 0x43, 0x6c, 0x61, 0x73, 0x73, + 0x4c, 0x69, 0x73, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, + 0x25, 0x0a, 0x09, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, + 0x63, 0x6c, 0x61, 0x73, 0x73, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x63, + 0x6c, 0x61, 0x73, 0x73, 0x65, 0x73, 0x42, 0x6a, 0x0a, 0x1f, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x2e, + 0x73, 0x74, 0x61, 0x63, 0x6b, 0x2e, 0x67, 0x61, 0x7a, 0x65, 0x6c, 0x6c, 0x65, 0x2e, 0x73, 0x63, + 0x61, 0x6c, 0x61, 0x2e, 0x70, 0x61, 0x72, 0x73, 0x65, 0x50, 0x01, 0x5a, 0x45, 0x67, 0x69, 0x74, + 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x62, 0x2f, 0x73, + 0x63, 0x61, 0x6c, 0x61, 0x2d, 0x67, 0x61, 0x7a, 0x65, 0x6c, 0x6c, 0x65, 0x2f, 0x62, 0x75, 0x69, + 0x6c, 0x64, 0x2f, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x2f, 0x67, 0x61, 0x7a, 0x65, 0x6c, 0x6c, 0x65, + 0x2f, 0x73, 0x63, 0x61, 0x6c, 0x61, 0x2f, 0x70, 0x61, 0x72, 0x73, 0x65, 0x3b, 0x70, 0x61, 0x72, + 0x73, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/build/stack/gazelle/scala/parse/file.proto b/build/stack/gazelle/scala/parse/file.proto index 004030a6..71f2e500 100644 --- a/build/stack/gazelle/scala/parse/file.proto +++ b/build/stack/gazelle/scala/parse/file.proto @@ -42,6 +42,8 @@ message File { // tree is a JSON string representing the parse tree. This field is only // populated when specifically requested during parsing. string tree = 14; + // sha256 is the sha256 hash value of the file contents + string sha256 = 15; } // ClassList represents a set of files. diff --git a/cmd/autokeep/BUILD.bazel b/cmd/autokeep/BUILD.bazel index 97f279f2..6246d459 100644 --- a/cmd/autokeep/BUILD.bazel +++ b/cmd/autokeep/BUILD.bazel @@ -6,7 +6,10 @@ go_library( srcs = ["autokeep.go"], importpath = "github.com/stackb/scala-gazelle/cmd/autokeep", visibility = ["//visibility:private"], - deps = ["//pkg/autokeep"], + deps = [ + "//pkg/autokeep", + "//pkg/resolver", + ], ) go_binary( diff --git a/cmd/autokeep/autokeep.go b/cmd/autokeep/autokeep.go index 654f4b90..9f6ae313 100644 --- a/cmd/autokeep/autokeep.go +++ b/cmd/autokeep/autokeep.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/stackb/scala-gazelle/pkg/autokeep" + "github.com/stackb/scala-gazelle/pkg/resolver" ) // autokeep is a program that consumes a scala-gazelle cache file, runs 'bazel @@ -103,7 +104,11 @@ func run(cfg *config) error { } log.Printf("diagnostics: %+v", diagnostics) - keep := autokeep.MakeDeltaDeps(cfg.deps, diagnostics) + + // FIXME(pcj): how to derive the global scope again from this command? + scope := resolver.NewTrieScope() + + keep := autokeep.MakeDeltaDeps(diagnostics, scope, cfg.deps) log.Printf("keep: %+v", keep) if err := autokeep.ApplyDeltaDeps(keep, cfg.keep); err != nil { diff --git a/cmd/scalafileextract/scalafileextract.go b/cmd/scalafileextract/scalafileextract.go index 6c0879f0..cdfb4ab2 100644 --- a/cmd/scalafileextract/scalafileextract.go +++ b/cmd/scalafileextract/scalafileextract.go @@ -106,7 +106,7 @@ func persistentWork(cfg *Config) error { batchCfg.Cwd = cfg.Cwd if err := batchWork(&batchCfg); err != nil { - return fmt.Errorf("performing persistent batch!: %v", err) + return fmt.Errorf("performing persistent batch: %v", err) } if err := protobuf.WriteDelimitedTo(&resp, os.Stdout); err != nil { @@ -200,6 +200,13 @@ func extract(parser *parser.ScalametaParser, dir string, sourceFiles []string) ( return nil, fmt.Errorf(file.Error) } file.Filename = filenames[file.Filename] + if false { + sha256, err := collections.FileSha256(file.Filename) + if err != nil { + return nil, err + } + file.Sha256 = sha256 + } } return response.Files, nil diff --git a/cmd/wildcardimportfixer/main.go b/cmd/wildcardimportfixer/main.go index 6bed3a2a..60e75258 100644 --- a/cmd/wildcardimportfixer/main.go +++ b/cmd/wildcardimportfixer/main.go @@ -66,7 +66,11 @@ func run(cfg *config) error { BazelExecutable: cfg.bazelExe, }) - symbols, err := fixer.Fix(cfg.ruleLabel, cfg.targetFilename, cfg.importPrefix) + symbols, err := fixer.Fix(&wildcardimport.FixConfig{ + RuleLabel: cfg.ruleLabel, + Filename: cfg.targetFilename, + ImportPrefix: cfg.importPrefix, + }) if err != nil { return err } diff --git a/go.mod b/go.mod index ea7e5beb..8bc8d266 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,7 @@ require ( github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/emicklei/proto v1.9.0 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect @@ -32,7 +33,7 @@ require ( github.com/rs/zerolog v1.34.0 // indirect github.com/stretchr/objx v0.1.0 // indirect golang.org/x/net v0.1.0 // indirect - golang.org/x/sys v0.12.0 // indirect + golang.org/x/sys v0.13.0 // indirect golang.org/x/text v0.4.0 // indirect golang.org/x/time v0.2.0 // indirect golang.org/x/tools v0.2.0 // indirect diff --git a/go.sum b/go.sum index 804dd00e..39c683bc 100644 --- a/go.sum +++ b/go.sum @@ -43,6 +43,8 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -163,6 +165,8 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= diff --git a/go_repos.bzl b/go_repos.bzl index 1f4184bb..97789535 100644 --- a/go_repos.bzl +++ b/go_repos.bzl @@ -252,8 +252,8 @@ def go_repositories(): name = "com_github_fsnotify_fsnotify", build_file_proto_mode = "disable_global", importpath = "github.com/fsnotify/fsnotify", - sum = "h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=", - version = "v1.6.0", + sum = "h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=", + version = "v1.9.0", ) go_repository( name = "com_github_ghodss_yaml", @@ -554,8 +554,8 @@ def go_repositories(): name = "org_golang_x_sys", build_file_proto_mode = "disable_global", importpath = "golang.org/x/sys", - sum = "h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=", - version = "v0.12.0", + sum = "h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=", + version = "v0.13.0", ) go_repository( name = "org_golang_x_term", diff --git a/language/scala/BUILD.bazel b/language/scala/BUILD.bazel index 8f7f208b..af2811cb 100644 --- a/language/scala/BUILD.bazel +++ b/language/scala/BUILD.bazel @@ -7,7 +7,6 @@ load("@bazel_gazelle//:def.bzl", "gazelle_binary") go_library( name = "scala", srcs = [ - "cache.go", "configure.go", "conflict_resolver_registry.go", "coverage.go", @@ -21,11 +20,13 @@ go_library( "imports.go", "kinds.go", "known_rule_registry.go", + "known_scope_registry.go", "language.go", "lifecycle.go", "loads.go", "package_marker_rule.go", "progress.go", + "repair.go", "resolve.go", "scala_package.go", "scala_rule.go", @@ -49,6 +50,7 @@ go_library( "//pkg/scalaconfig", "//pkg/scalafiles", "//pkg/scalarule", + "//pkg/sweep", "//pkg/wildcardimport", "@bazel_gazelle//config:go_default_library", "@bazel_gazelle//label:go_default_library", @@ -85,10 +87,6 @@ gazelle_binary( go_test( name = "scala_test", srcs = [ - # "coverage_test.go", - # "diff_test.go", - # "existing_scala_rule_test.go", - # "flags_test.go", "coverage_test.go", "diff_test.go", "existing_scala_rule_test.go", @@ -133,7 +131,6 @@ package_filegroup( name = "filegroup", srcs = [ "BUILD.bazel", - "cache.go", "configure.go", "conflict_resolver_registry.go", "coverage.go", @@ -152,6 +149,7 @@ package_filegroup( "imports.go", "kinds.go", "known_rule_registry.go", + "known_scope_registry.go", "language.go", "language_test.go", "lifecycle.go", @@ -159,6 +157,7 @@ package_filegroup( "loads_test.go", "package_marker_rule.go", "progress.go", + "repair.go", "resolve.go", "scala.WORKSPACE", "scala_package.go", diff --git a/language/scala/cache.go b/language/scala/cache.go deleted file mode 100644 index 398a86a2..00000000 --- a/language/scala/cache.go +++ /dev/null @@ -1,60 +0,0 @@ -package scala - -import ( - "log" - "time" - - "github.com/bazelbuild/bazel-gazelle/label" - scpb "github.com/stackb/scala-gazelle/build/stack/gazelle/scala/cache" - "github.com/stackb/scala-gazelle/pkg/parser" - "github.com/stackb/scala-gazelle/pkg/protobuf" -) - -const debugCache = false - -func (sl *scalaLang) readScalaRuleCacheFile() error { - t1 := time.Now() - - if err := protobuf.ReadFile(sl.cacheFileFlagValue, &sl.cache); err != nil { - return err - } - - if sl.cacheKeyFlagValue != sl.cache.Key { - if debugCache { - log.Printf("scala-gazelle cache invalidated! (want %q, got %q)", sl.cacheFileFlagValue, sl.cache.Key) - } - sl.cache = scpb.Cache{} - return nil - } - - parser.SortRules(sl.cache.Rules) - - for _, rule := range sl.cache.Rules { - from, err := label.Parse(rule.Label) - if err != nil { - return err - } - if err := sl.parser.LoadScalaRule(from, rule); err != nil { - return err - } - } - - if debugCache { - t2 := time.Since(t1).Round(1 * time.Millisecond) - log.Printf("Read cache %s (%d rules) %v", sl.cacheFileFlagValue, len(sl.cache.Rules), t2) - } - - return nil -} - -func (sl *scalaLang) writeScalaRuleCacheFile() error { - sl.cache.PackageCount = int32(len(sl.packages)) - sl.cache.Rules = sl.parser.ScalaRules() - sl.cache.Key = sl.cacheKeyFlagValue - - if debugCache { - log.Printf("Wrote scala-gazelle cache %s (%d rules)", sl.cacheFileFlagValue, len(sl.cache.Rules)) - } - - return protobuf.WriteFile(sl.cacheFileFlagValue, &sl.cache) -} diff --git a/language/scala/environment.go b/language/scala/environment.go index 9db1a297..96e1aeed 100644 --- a/language/scala/environment.go +++ b/language/scala/environment.go @@ -1,10 +1,21 @@ package scala -import "github.com/stackb/scala-gazelle/pkg/procutil" +import ( + "os" + + "github.com/stackb/scala-gazelle/pkg/procutil" +) const ( SCALA_GAZELLE_LOG_FILE = procutil.EnvVar("SCALA_GAZELLE_LOG_FILE") SCALA_GAZELLE_SHOW_COVERAGE = procutil.EnvVar("SCALA_GAZELLE_SHOW_COVERAGE") SCALA_GAZELLE_SHOW_PROGRESS = procutil.EnvVar("SCALA_GAZELLE_SHOW_PROGRESS") + SCALA_GAZELLE_WATCH_DIR = procutil.EnvVar("SCALA_GAZELLE_WATCH_DIR") TEST_TMPDIR = procutil.EnvVar("TEST_TMPDIR") ) + +func PrintEnv(printf func(format string, args ...any)) { + for _, env := range os.Environ() { + printf("env: %s", env) + } +} diff --git a/language/scala/existing_scala_rule.go b/language/scala/existing_scala_rule.go index f48e8b12..0b39ceec 100644 --- a/language/scala/existing_scala_rule.go +++ b/language/scala/existing_scala_rule.go @@ -12,11 +12,10 @@ import ( "github.com/bazelbuild/bazel-gazelle/rule" "github.com/bazelbuild/buildtools/build" - "github.com/stackb/scala-gazelle/pkg/bazel" - "github.com/stackb/scala-gazelle/pkg/collections" "github.com/stackb/scala-gazelle/pkg/protobuf" "github.com/stackb/scala-gazelle/pkg/scalaconfig" "github.com/stackb/scala-gazelle/pkg/scalarule" + "github.com/stackb/scala-gazelle/pkg/sweep" sppb "github.com/stackb/scala-gazelle/build/stack/gazelle/scala/parse" ) @@ -76,24 +75,20 @@ func (s *existingScalaRuleProvider) ProvideRule(cfg *scalarule.Config, pkg scala // ResolveRule implements the RuleResolver interface. func (s *existingScalaRuleProvider) ResolveRule(cfg *scalarule.Config, pkg scalarule.Package, r *rule.Rule) scalarule.RuleProvider { - scalaRule, err := pkg.ParseRule(r, "srcs") + scalaRule, err := pkg.NewScalaRule(r) if err != nil { if err != ErrRuleHasNoSrcs { log.Printf("skipping %s %s: unable to collect srcs: %v", r.Kind(), r.Name(), err) return nil } - // rule has no srcs. This is OK for binary rules, sometimes they only - // have a main_class. - // if !s.isBinary { - // return nil // no need to print a warning - // } } if scalaRule == nil { log.Panicln("scalaRule should not be nil!") } + // stash the scalaRule instance as the GazelleImportsKey so that it triggers + // the resolve phase. r.SetPrivateAttr(config.GazelleImportsKey, scalaRule) - r.SetPrivateAttr("_scala_files", scalaRule.Files()) return &existingScalaRule{cfg, pkg, r, scalaRule, s.isBinary, s.isLibrary, s.isTest} } @@ -137,131 +132,50 @@ func (s *existingScalaRule) Resolve(rctx *scalarule.ResolveContext, importsRaw i } sc := scalaconfig.Get(rctx.Config) - imports := scalaRule.ResolveImports(rctx) - sc.Imports(imports, rctx.Rule, "deps", rctx.From) - - commentsSrcs := rctx.Rule.AttrComments("srcs") - if commentsSrcs != nil { - commentsSrcs.Before = nil - if sc.ShouldAnnotateImports() { - scalaconfig.AnnotateImports(imports, commentsSrcs, "import: ") - } - if sc.ShouldAnnotateRule() { - ruleComments := makeRuleComments(scalaRule.pb) - commentsSrcs.Before = append(commentsSrcs.Before, ruleComments...) - } - } - if s.isLibrary { - exports := scalaRule.ResolveExports(rctx) - sc.Exports(exports, rctx.Rule, "exports", rctx.From) - } + resolveClosure := func() { + imports := scalaRule.ResolveImports(rctx) - if sc.ShouldSweepTransitive("deps") { - if !hasTransitiveComment(rctx.Rule) { - if junk, err := s.sweepTransitiveAttr("deps", rctx.Rule, rctx.From); err != nil { - log.Printf("warning: transitive sweep failed: %v", err) - } else { - if len(junk) > 0 { - log.Println(formatBuildozerRemoveDeps(rctx.From, junk)) - } + sc.MergeDepsAttr(imports, rctx.Rule, "deps", rctx.From) + + commentsSrcs := rctx.Rule.AttrComments("srcs") + if commentsSrcs != nil { + commentsSrcs.Before = nil + if sc.ShouldAnnotateImports() { + scalaconfig.AnnotateImports(imports, commentsSrcs, "import: ") + } + if sc.ShouldAnnotateRule() { + ruleComments := makeRuleComments(scalaRule.pb) + commentsSrcs.Before = append(commentsSrcs.Before, ruleComments...) } - rctx.Rule.AddComment(scalaconfig.TransitiveCommentToken) - } else { - log.Println("> transitive sweep skipped (already done):", rctx.From) } - } -} -// sweepTransitiveDeps iterates through deps marked "UNKNOWN" and removes them -// if the target still builds without it. -func (s *existingScalaRule) sweepTransitiveAttr(attrName string, r *rule.Rule, from label.Label) ([]string, error) { - // get the File to which this Rule belongs - file := s.pkg.GenerateArgs().File - - return s.sweepTransitive(attrName, file, r, from) -} - -// sweepTransitive iterates through deps marked "UNKNOWN" and removes them if -// the target still builds without it. -func (s *existingScalaRule) sweepTransitive(attrName string, file *rule.File, r *rule.Rule, from label.Label) (junk []string, err error) { - expr := r.Attr(attrName) - if expr == nil { - return nil, nil - } - - deps, isList := expr.(*build.ListExpr) - if !isList { - return nil, nil // some other condition we can't deal with - } + if s.isLibrary { + exports := scalaRule.ResolveExports(rctx) + sc.MergeDepsAttr(exports, rctx.Rule, "exports", rctx.From) + } - // check that the deps have at least one unknown dep - var hasTransitiveDeps bool - for _, expr := range deps.List { - if str, ok := expr.(*build.StringExpr); ok { - for _, suffix := range str.Comment().Suffix { - if suffix.Token == scalaconfig.TransitiveCommentToken { - hasTransitiveDeps = true - break - } + if sc.ShouldSweepTransitive(rctx.Rule, "deps") { + if _, err := sweep.UnknownAttr("deps", rctx.File, rctx.Rule, rctx.From); err != nil { + log.Printf("warning: transitive sweep failed: %v", err) + } else { + // sweep.RemoveSweepTransitiveDepsTag(rctx.Rule) + sweep.AddPostGazelleBuildozerCommand("remove tags "+sweep.SweepTransitiveDepsTag, rctx.From.String()) } } } - if !hasTransitiveDeps { - return nil, nil // nothing to do - } - - // target should build first time, otherwise we can't check accurately. - log.Println("🧱 transitive sweep:", from) - - if out, exitCode, _ := bazel.ExecCommand("bazel", "build", from.String()); exitCode != 0 { - log.Fatalln("sweep failed (must build cleanly on first attempt): %s", string(out)) - } - - // iterate the list backwards - for i := len(deps.List) - 1; i >= 0; i-- { - expr := deps.List[i] - // look for transitive string dep expressions - dep, ok := expr.(*build.StringExpr) - if !ok { - continue - } - var isTransitiveDep bool - for _, suffix := range dep.Comment().Suffix { - if suffix.Token == scalaconfig.TransitiveCommentToken { - isTransitiveDep = true - break - } - } - if !isTransitiveDep { - continue - } + // wow... this cannot be good programming practive, but I need to store the + // ability to re-resolve dependencies, so stashing this in the rule... + rctx.Rule.SetPrivateAttr("_scala_resolve_closure", resolveClosure) - // reference of original list in case it does not build - original := deps.List - // reset deps with this one spliced out - deps.List = collections.SliceRemoveIndex(deps.List, i) - // save file to reflect change - if err := file.Save(file.Path); err != nil { - return nil, err - } - // see if it still builds - if _, exitCode, _ := bazel.ExecCommand("bazel", "build", from.String()); exitCode == 0 { - log.Println("- 💩 junk:", dep.Value) - junk = append(junk, dep.Value) - } else { - log.Println("- 👑 keep:", dep.Value) - deps.List = original - } - } + resolveClosure() - // final save with possible last change - if err := file.Save(file.Path); err != nil { - return nil, err + if sc.ShouldRepairTransitiveDeps() { + log.Println("Marking", rctx.From) + // mark this rule as needing to be repaired + sweep.MarkForTransitiveRepair(rctx.Rule, rctx.From, sc.MaybeRewrite(rctx.Rule.Kind(), rctx.From)) } - - return } func makeRuleComments(pb *sppb.Rule) (comments []build.Comment) { @@ -281,12 +195,3 @@ func makeRuleComments(pb *sppb.Rule) (comments []build.Comment) { func formatBuildozerRemoveDeps(from label.Label, junk []string) string { return fmt.Sprintf("buildozer 'remove deps %s' %s", strings.Join(junk, " "), from.String()) } - -func hasTransitiveComment(r *rule.Rule) bool { - for _, before := range r.Comments() { - if before == scalaconfig.TransitiveCommentToken { - return true - } - } - return false -} diff --git a/language/scala/flags.go b/language/scala/flags.go index 4a6a10b3..b761cf50 100644 --- a/language/scala/flags.go +++ b/language/scala/flags.go @@ -1,10 +1,8 @@ package scala import ( - "errors" "flag" "fmt" - "io/fs" "log" "os" "path/filepath" @@ -28,8 +26,8 @@ const ( scalaGazelleCacheFileFlagName = "scala_gazelle_cache_file" scalaGazelleImportsFileFlagName = "scala_gazelle_imports_file" scalaGazelleDebugProcessFileFlagName = "scala_gazelle_debug_process" - scalaGazelleCacheKeyFlagName = "scala_gazelle_cache_key" scalaGazellePrintCacheKeyFlagName = "scala_gazelle_print_cache_key" + scalaGazelleFixDepsModeFlagName = "scala_gazelle_fix_deps_mode" cpuprofileFileFlagName = "cpuprofile_file" memprofileFileFlagName = "memprofile_file" logFileFlagName = "log_file" @@ -44,7 +42,6 @@ func (sl *scalaLang) RegisterFlags(flags *flag.FlagSet, cmd string, c *config.Co flags.BoolVar(&sl.existingScalaRuleCoverageFlagValue, existingScalaRuleCoverageFlagName, true, "report coverage statistics") flags.StringVar(&sl.cacheFileFlagValue, scalaGazelleCacheFileFlagName, "", "optional path a cache file (.json or .pb)") flags.StringVar(&sl.importsFileFlagValue, scalaGazelleImportsFileFlagName, "", "optional path to an imports file where resolved imports should be written (.json or .pb)") - flags.StringVar(&sl.cacheKeyFlagValue, scalaGazelleCacheKeyFlagName, "", "optional string that can be used to bust the cache file") flags.StringVar(&sl.cpuprofileFlagValue, cpuprofileFileFlagName, "", "optional path a cpuprofile file (.prof)") flags.StringVar(&sl.memprofileFlagValue, memprofileFileFlagName, "", "optional path a memory profile file (.prof)") flags.Var(&sl.symbolProviderNamesFlagValue, scalaSymbolProviderFlagName, "name of a symbol provider implementation to enable") @@ -53,6 +50,7 @@ func (sl *scalaLang) RegisterFlags(flags *flag.FlagSet, cmd string, c *config.Co flags.Var(&sl.existingScalaBinaryRulesFlagValue, existingScalaBinaryRuleFlagName, "LOAD%NAME mapping for a custom existing scala binary rule implementation (e.g. '@io_bazel_rules_scala//scala:scala.bzl%scalabinary'") flags.Var(&sl.existingScalaLibraryRulesFlagValue, existingScalaLibraryRuleFlagName, "LOAD%NAME mapping for a custom existing scala library rule implementation (e.g. '@io_bazel_rules_scala//scala:scala.bzl%scala_library'") flags.Var(&sl.existingScalaTestRulesFlagValue, existingScalaTestRuleFlagName, "LOAD%NAME mapping for a custom existing scala test rule implementation (e.g. '@io_bazel_rules_scala//scala:scala.bzl%scala_test'") + flags.Var(&sl.repairMode, scalaGazelleFixDepsModeFlagName, "optional deps repair mode (one of 'none', 'batch', 'watch', 'transitive')") sl.registerSymbolProviders(flags, cmd, c) sl.registerConflictResolvers(flags, cmd, c) @@ -79,6 +77,7 @@ func (sl *scalaLang) CheckFlags(flags *flag.FlagSet, c *config.Config) error { collections.PrintProcessIdForDelveAndWait() } + sl.repoRoot = c.RepoRoot sl.symbolResolver = newUniverseResolver(sl, sl.globalPackages) if err := sl.setupSymbolProviders(flags, c, sl.symbolProviderNamesFlagValue); err != nil { @@ -99,9 +98,6 @@ func (sl *scalaLang) CheckFlags(flags *flag.FlagSet, c *config.Config) error { if err := sl.setupExistingScalaTestRules(sl.existingScalaTestRulesFlagValue); err != nil { return err } - if err := sl.setupCache(); err != nil { - return err - } if err := sl.setupCpuProfiling(c.WorkDir); err != nil { return err } @@ -240,19 +236,6 @@ func (sl *scalaLang) setupExistingScalaTestRule(fqn, load, kind string) error { return sl.ruleProviderRegistry.RegisterProvider(fqn, provider) } -func (sl *scalaLang) setupCache() error { - if sl.cacheFileFlagValue != "" { - sl.cacheFileFlagValue = os.ExpandEnv(sl.cacheFileFlagValue) - if err := sl.readScalaRuleCacheFile(); err != nil { - // don't report error if the file does not exist yet - if !errors.Is(err, fs.ErrNotExist) { - return fmt.Errorf("reading cache file: %w", err) - } - } - } - return nil -} - func (sl *scalaLang) dumpResolvedImportMap() { if sl.importsFileFlagValue == "" { return diff --git a/language/scala/flags_test.go b/language/scala/flags_test.go index 9c8eb2d4..c04e54e3 100644 --- a/language/scala/flags_test.go +++ b/language/scala/flags_test.go @@ -1,153 +1,16 @@ package scala import ( - "flag" "fmt" - "os" - "strings" "testing" - "github.com/bazelbuild/bazel-gazelle/config" "github.com/bazelbuild/bazel-gazelle/rule" - "github.com/bazelbuild/bazel-gazelle/testtools" "github.com/google/go-cmp/cmp" "github.com/stackb/scala-gazelle/pkg/scalarule" "github.com/stackb/scala-gazelle/pkg/testutil" ) -func TestCacheFlags(t *testing.T) { - for name, tc := range map[string]struct { - args []string - files []testtools.FileSpec - wantErr error - check func(t *testing.T, tmpDir string, lang *scalaLang) - }{ - "scala_gazelle_cache_file": { - files: []testtools.FileSpec{ - { - Path: "maven_install.json", - Content: "{}", - }, - { - Path: "./cache.json", - Content: `{"package_count": 100}`, - }, - }, - args: []string{ - "-maven_install_json_file=./maven_install.json", - "-scala_gazelle_cache_file=${TEST_TMPDIR}/cache.json", - }, - check: func(t *testing.T, tmpDir string, lang *scalaLang) { - cacheFile := strings.TrimPrefix(strings.TrimPrefix(lang.cacheFileFlagValue, tmpDir), "/") - if diff := cmp.Diff("cache.json", cacheFile); diff != "" { - t.Errorf("cacheFile (-want got):\n%s", diff) - } - if diff := cmp.Diff(int32(100), lang.cache.PackageCount); diff != "" { - t.Errorf("PackageCount (-want got):\n%s", diff) - } - }, - }, - "scala_gazelle_print_cache_key_on": { - args: []string{ - "-scala_gazelle_cache_key=12345", - "-scala_gazelle_print_cache_key=true", - }, - check: func(t *testing.T, tmpDir string, lang *scalaLang) { - if diff := cmp.Diff("12345", lang.cacheKeyFlagValue); diff != "" { - t.Errorf("cacheKeyFlagValue (-want got):\n%s", diff) - } - if diff := cmp.Diff(true, lang.printCacheKey); diff != "" { - t.Errorf("printCacheKey (-want got):\n%s", diff) - } - }, - }, - "scala_gazelle_print_cache_key_off": { - args: []string{ - "-scala_gazelle_print_cache_key=false", - }, - check: func(t *testing.T, tmpDir string, lang *scalaLang) { - if diff := cmp.Diff(false, lang.printCacheKey); diff != "" { - t.Errorf("printCacheKey (-want got):\n%s", diff) - } - }, - }, - "scala_gazelle_cache_key__valid": { - files: []testtools.FileSpec{ - { - Path: "maven_install.json", - Content: "{}", - }, - { - Path: "./cache.json", - Content: `{"package_count": 100, "key": "1"}`, - }, - }, - args: []string{ - "-maven_install_json_file=./maven_install.json", - "-scala_gazelle_cache_file=${TEST_TMPDIR}/cache.json", - "-scala_gazelle_cache_key=1", - }, - check: func(t *testing.T, tmpDir string, lang *scalaLang) { - if lang.cache.PackageCount == 0 { - t.Error("expected cache to be valid!") - } - }, - }, - "scala_gazelle_cache_key__invalid": { - files: []testtools.FileSpec{ - { - Path: "maven_install.json", - Content: "{}", - }, - { - Path: "./cache.json", - Content: `{"package_count": 100, "key": "1"}`, - }, - }, - args: []string{ - "-maven_install_json_file=./maven_install.json", - "-scala_gazelle_cache_file=${TEST_TMPDIR}/cache.json", - "-scala_gazelle_cache_key=2", - }, - check: func(t *testing.T, tmpDir string, lang *scalaLang) { - if lang.cache.PackageCount != 0 { - t.Error("expected cache to be invalidated!") - } - }, - }, - } { - t.Run(name, func(t *testing.T) { - tmpDir, _, cleanup := testutil.MustPrepareTestFiles(t, tc.files) - if true { - defer cleanup() - } - - os.Setenv("TEST_TMPDIR", tmpDir) - lang := NewLanguage().(*scalaLang) - - fs := flag.NewFlagSet(scalaLangName, flag.ExitOnError) - c := &config.Config{ - WorkDir: tmpDir, - Exts: make(map[string]interface{}), - } - - lang.RegisterFlags(fs, "", c) - if err := fs.Parse(tc.args); err != nil { - t.Fatal(err) - } - - if err := lang.CheckFlags(fs, c); err != nil { - t.Fatal(err) - } - - if tc.check != nil { - tc.check(t, tmpDir, lang) - } - }) - } -} - func TestParseScalaExistingRules(t *testing.T) { for name, tc := range map[string]struct { providerNames []string diff --git a/language/scala/generate.go b/language/scala/generate.go index d3039633..148184d6 100644 --- a/language/scala/generate.go +++ b/language/scala/generate.go @@ -23,10 +23,6 @@ func (sl *scalaLang) GenerateRules(args language.GenerateArgs) (result language. return } - if sl.wantProgress && sl.cache.PackageCount > 0 { - writeGenerateProgress(sl.progress, len(sl.packages), int(sl.cache.PackageCount)) - } - logger := sl.logger.With().Str("rel", args.Rel).Logger() pkg := newScalaPackage(logger, args, sc, sl.ruleProviderRegistry, sl.parser, sl) sl.packages[args.Rel] = pkg diff --git a/language/scala/imports.go b/language/scala/imports.go index 515f18c4..0bbea231 100644 --- a/language/scala/imports.go +++ b/language/scala/imports.go @@ -9,14 +9,15 @@ import ( "github.com/bazelbuild/bazel-gazelle/label" scpb "github.com/stackb/scala-gazelle/build/stack/gazelle/scala/cache" "github.com/stackb/scala-gazelle/pkg/protobuf" + "github.com/stackb/scala-gazelle/pkg/resolver" ) -func (sl *scalaLang) writeResolvedImportsMapFile(filename string) error { +func makeResolvedImports(scope resolver.Scope) *scpb.ResolvedImports { imports := &scpb.ResolvedImports{ Imports: make(map[string]string), } - for _, sym := range sl.globalScope.GetSymbols("") { + for _, sym := range scope.GetSymbols("") { dep := "NO_LABEL" if sym.Label != label.NoLabel { dep = sym.Label.String() @@ -24,6 +25,12 @@ func (sl *scalaLang) writeResolvedImportsMapFile(filename string) error { imports.Imports[sym.Name] = dep } + return imports +} + +func (sl *scalaLang) writeResolvedImportsMapFile(filename string) error { + imports := makeResolvedImports(sl.globalScope) + if filepath.Ext(filename) == ".txt" { f, err := os.Create(filename) if err != nil { diff --git a/language/scala/known_rule_registry.go b/language/scala/known_rule_registry.go index 2e444048..7aa6be75 100644 --- a/language/scala/known_rule_registry.go +++ b/language/scala/known_rule_registry.go @@ -7,15 +7,13 @@ import ( "github.com/bazelbuild/bazel-gazelle/rule" ) -// GetKnownRule implements part of the -// resolver.KnownRuleRegistry interface. +// GetKnownRule implements part of the resolver.KnownRuleRegistry interface. func (sl *scalaLang) GetKnownRule(from label.Label) (*rule.Rule, bool) { r, ok := sl.knownRules[from] return r, ok } -// PutKnownRule implements part of the -// resolver.KnownRuleRegistry interface. +// PutKnownRule implements part of the resolver.KnownRuleRegistry interface. func (sl *scalaLang) PutKnownRule(from label.Label, r *rule.Rule) error { if _, ok := sl.knownRules[from]; ok { return fmt.Errorf("duplicate known rule: %s", from) diff --git a/language/scala/known_scope_registry.go b/language/scala/known_scope_registry.go new file mode 100644 index 00000000..2dd6a042 --- /dev/null +++ b/language/scala/known_scope_registry.go @@ -0,0 +1,22 @@ +package scala + +import ( + "fmt" + + "github.com/stackb/scala-gazelle/pkg/resolver" +) + +// GetKnownScope implements part of the resolver.KnownScopeRegistry interface. +func (sl *scalaLang) GetKnownScope(name string) (resolver.Scope, bool) { + scope, ok := sl.knownScopes[name] + return scope, ok +} + +// PutKnownScope implements part of the resolver.KnownScopeRegistry interface. +func (sl *scalaLang) PutKnownScope(name string, scope resolver.Scope) error { + if _, ok := sl.knownScopes[name]; ok { + return fmt.Errorf("duplicate known rule: %s", name) + } + sl.knownScopes[name] = scope + return nil +} diff --git a/language/scala/language.go b/language/scala/language.go index d1a1ac32..82a26ab2 100644 --- a/language/scala/language.go +++ b/language/scala/language.go @@ -12,7 +12,6 @@ import ( "github.com/rs/zerolog" "github.com/stackb/rules_proto/pkg/protoc" - scpb "github.com/stackb/scala-gazelle/build/stack/gazelle/scala/cache" "github.com/stackb/scala-gazelle/pkg/collections" "github.com/stackb/scala-gazelle/pkg/parser" "github.com/stackb/scala-gazelle/pkg/procutil" @@ -27,6 +26,8 @@ const scalaLangName = "scala" // scalaLang implements language.Language. type scalaLang struct { + // repoRoot is the config.RepoRoot + repoRoot string // debugProcessFlagValue halts processing and prints the PID for attaching a // delve debugger. debugProcessFlagValue bool @@ -56,12 +57,12 @@ type scalaLang struct { existingScalaLibraryRulesFlagValue collections.StringSlice // existingScalaLibraryRulesFlagValue is the value of the // existing_scala_test_rule repeatable flag - existingScalaTestRulesFlagValue collections.StringSlice + existingScalaTestRulesFlagValue collections.StringSlice + // repairMode for fixing scala deps + repairMode repairMode cpuprofileFlagValue string existingScalaRuleCoverageFlagValue bool memprofileFlagValue string - // cache is the loaded cache, if configured - cache scpb.Cache // ruleProviderRegistry is the rule registry implementation. This holds the // rules configured via gazelle directives by the user. ruleProviderRegistry scalarule.ProviderRegistry @@ -80,6 +81,8 @@ type scalaLang struct { progress mobyprogress.Output // knownRules is a map of all known generated rules knownRules map[label.Label]*rule.Rule + // knownScopes is a map of all known scopes + knownScopes map[string]resolver.Scope // conflictResolvers is a map of all known conflict resolver implementations conflictResolvers map[string]resolver.ConflictResolver // depsCleaners is a map of all known deps cleaner implementations @@ -97,7 +100,7 @@ type scalaLang struct { // sourceProvider is the sourceProvider implementation. sourceProvider *provider.SourceProvider // parser is the parser instance - parser *parser.MemoParser + parser parser.Parser // logFileName is the name of the log file // logFile is the open log logFile *os.File @@ -112,10 +115,10 @@ func NewLanguage() language.Language { lang := &scalaLang{ wantProgress: wantProgress(), - cache: scpb.Cache{}, globalScope: resolver.NewTrieScope(), globalPackages: resolver.NewTrieScope(), knownRules: make(map[label.Label]*rule.Rule), + knownScopes: make(map[string]resolver.Scope), conflictResolvers: make(map[string]resolver.ConflictResolver), depsCleaners: make(map[string]resolver.DepsCleaner), packages: make(map[string]*scalaPackage), @@ -135,7 +138,7 @@ func NewLanguage() language.Language { lang.sourceProvider = provider.NewSourceProvider(logger.With().Str("provider", "source").Logger(), progress) semanticProvider := provider.NewSemanticdbProvider(lang.sourceProvider) - lang.parser = parser.NewMemoParser(semanticProvider) + lang.parser = semanticProvider javaProvider := provider.NewJavaProvider() lang.AddSymbolProvider(lang.sourceProvider) diff --git a/language/scala/lifecycle.go b/language/scala/lifecycle.go index 755f5b57..00b827a6 100644 --- a/language/scala/lifecycle.go +++ b/language/scala/lifecycle.go @@ -23,12 +23,6 @@ func (sl *scalaLang) onResolve() { } else { sl.globalScope = scalaScope } - - if sl.cacheFileFlagValue != "" { - if err := sl.writeScalaRuleCacheFile(); err != nil { - log.Fatalf("failed to write cache: %v", err) - } - } } // onEnd is called when the last rule has been resolved. @@ -40,7 +34,13 @@ func (sl *scalaLang) onEnd() { log.Fatalf("provider.OnEnd transition error %s: %v", provider.Name(), err) } } + for _, pkg := range sl.packages { + if err := pkg.OnEnd(); err != nil { + log.Fatalf("pkg.OnEnd transition error %s: %v", pkg.args.Rel, err) + } + } + sl.repair() sl.dumpResolvedImportMap() sl.reportCoverage(log.Printf) sl.stopCpuProfiling() diff --git a/language/scala/repair.go b/language/scala/repair.go new file mode 100644 index 00000000..fad4328e --- /dev/null +++ b/language/scala/repair.go @@ -0,0 +1,285 @@ +package scala + +import ( + "fmt" + "log" + "os" + "os/exec" + "path" + "strings" + + "github.com/bazelbuild/bazel-gazelle/label" + "github.com/bazelbuild/bazel-gazelle/rule" + "github.com/stackb/scala-gazelle/pkg/bazel" + "github.com/stackb/scala-gazelle/pkg/procutil" + "github.com/stackb/scala-gazelle/pkg/scalarule" + "github.com/stackb/scala-gazelle/pkg/sweep" +) + +type repairMode int + +const ( + RepairNone repairMode = iota + RepairBatch + RepairWatch + RepairTransitive +) + +var ( + CI = procutil.EnvVar("CI") + SCALA_GAZELLE_BUILDOZER_FILE = procutil.EnvVar("SCALA_GAZELLE_BUILDOZER_FILE") +) + +// String partially implements the flag.Value interface. +func (i *repairMode) String() string { + switch *i { + case RepairNone: + return "none" + case RepairBatch: + return "batch" + case RepairWatch: + return "watch" + case RepairTransitive: + return "transitive" + } + return "unknown" +} + +func maybeGitCommitAndPushBuildFiles() error { + if !isCI() { + return nil + } + return gitCommitAndPushBuildFiles("build(gazelle): sweep transitive deps") +} + +// Set implements the flag.Value interface. +func (i *repairMode) Set(value string) error { + switch value { + case "", "none": + *i = RepairNone + case "batch": + *i = RepairBatch + case "watch": + *i = RepairWatch + case "transitive": + *i = RepairTransitive + default: + return fmt.Errorf("unknown repair value: %s", value) + } + return nil +} + +func (sl *scalaLang) repair() { + if err := sl.maybeWritePostGazelleBuildozerFile(); err != nil { + log.Printf("post-gazelle buildozer file write error: %v", err) + } + + if err := sl.repairDeps(sl.repairMode); err != nil { + log.Printf("warning: repair failed: %v", err) + } +} + +func (sl *scalaLang) maybeWritePostGazelleBuildozerFile() error { + if filename, ok := procutil.LookupEnv(SCALA_GAZELLE_BUILDOZER_FILE); ok { + if err := sweep.WritePostGazelleBuildozerFile(filename); err != nil { + return err + } + } + return nil +} + +func (sl *scalaLang) repairDeps(mode repairMode) error { + if err := maybeGitCommitAndPushBuildFiles(); err != nil { + return err + } + + switch mode { + case RepairBatch: + return sl.repairBatch() + case RepairWatch: + return sl.repairWatch() + case RepairTransitive: + return sl.repairTransitive() + default: + return nil + } +} + +func (sl *scalaLang) repairBatch() error { + rules := gatherResolvableScalaRuleMap(sl.knownRules) + imports := makeResolvedImports(sl.globalScope) + + fixer := sweep.NewDepFixer(sl.progress, sl.repoRoot, "", rules, imports.Imports, sl, sl.globalScope) + return fixer.Batch() +} + +func (sl *scalaLang) repairWatch() error { + dir, ok := procutil.LookupEnv(SCALA_GAZELLE_WATCH_DIR) + if !ok { + return fmt.Errorf("error: %v must be set to the directory to watch", SCALA_GAZELLE_WATCH_DIR) + } + if !path.IsAbs(dir) { + dir = path.Join(sl.repoRoot, dir) + } + + rules := gatherResolvableScalaRuleMap(sl.knownRules) + imports := makeResolvedImports(sl.globalScope) + + fixer := sweep.NewDepFixer(sl.progress, sl.repoRoot, "", rules, imports.Imports, sl, sl.globalScope) + + return fixer.Watch(dir) +} + +func (sl *scalaLang) repairTransitive() error { + rules := gatherResolvableScalaRuleMap(sl.knownRules) + imports := makeResolvedImports(sl.globalScope) + + fixer := sweep.NewDepFixer(sl.progress, sl.repoRoot, "", rules, imports.Imports, sl, sl.globalScope) + + return fixer.Transitive() +} + +func gatherResolvableScalaRuleMap(knownRules map[label.Label]*rule.Rule) sweep.ResolvableScalaRuleMap { + scalaRules := make(sweep.ResolvableScalaRuleMap) + + for _, knownRule := range knownRules { + scalaRule, ok := scalarule.GetRule(knownRule) + if !ok { + continue + } + resolveFunc := knownRule.PrivateAttr("_scala_resolve_closure").(func()) + scalaRules[scalaRule] = resolveFunc + } + + return scalaRules +} + +func isCI() bool { + return procutil.LookupBoolEnv(CI, false) +} + +func gitCommit() error { + cmd := exec.Command("git", "commit", "-am", "build(gazelle): sweep transitive deps") + cmd.Dir = bazel.GetBuildWorkspaceDirectory() + + output, err := cmd.CombinedOutput() + exitCode := procutil.CmdExitCode(cmd, err) + if exitCode != 0 { + return fmt.Errorf("git commit failed: %v\n%s", err, string(output)) + } + + return nil +} + +func gitHasChanges() (bool, error) { + // Check for uncommitted changes + cmd := exec.Command("git", "diff", "--quiet", "HEAD") + cmd.Dir = bazel.GetBuildWorkspaceDirectory() + + err := cmd.Run() + exitCode := procutil.CmdExitCode(cmd, err) + + if exitCode == 0 { + // Exit code 0 means no changes (diff is empty) + return false, nil + } else if exitCode == 1 { + // Exit code 1 from git diff --quiet means changes exist + return true, nil + } else { + // Any other exit code indicates an error + output, _ := exec.Command("git", "diff", "HEAD").CombinedOutput() + return false, fmt.Errorf("git diff failed with exit code %d: %v\n%s", exitCode, err, string(output)) + } +} + +func gitPush() error { + cmd := exec.Command("git", "push", "origin", "HEAD") + cmd.Dir = bazel.GetBuildWorkspaceDirectory() + + output, err := cmd.CombinedOutput() + exitCode := procutil.CmdExitCode(cmd, err) + if exitCode != 0 { + return fmt.Errorf("git push failed: %v\n%s", err, string(output)) + } + + return nil +} + +// gitCommitAndPushBuildFiles commits only BUILD.bazel files with the given message +// and skips pre-commit hooks +func gitCommitAndPushBuildFiles(message string) error { + repoDir := bazel.GetBuildWorkspaceDirectory() + + // + // FIND CHANGED FILES + // + + // Find all changed BUILD.bazel files in the repository + findCmd := exec.Command("git", "ls-files", "--modified", "--exclude-standard", "*BUILD.bazel") + findCmd.Dir = repoDir + + buildFiles, findErr := findCmd.Output() + findExitCode := procutil.CmdExitCode(findCmd, findErr) + if findExitCode != 0 { + return fmt.Errorf("finding BUILD.bazel files failed: %v", findErr) + } + + // If no BUILD.bazel files found, nothing to commit + if len(strings.TrimSpace(string(buildFiles))) == 0 { + return nil + } + + // + // ADD FILES + // + + // Convert the output to a list of files + fileList := strings.Fields(string(buildFiles)) + + // Add all BUILD.bazel files in a single command + addCmd := exec.Command("git", append([]string{"add"}, fileList...)...) + addCmd.Dir = repoDir + + addOutput, addErr := addCmd.CombinedOutput() + addExitCode := procutil.CmdExitCode(addCmd, addErr) + if addExitCode != 0 { + return fmt.Errorf("git add failed: %v\n%s", addErr, string(addOutput)) + } + + // + // COMMIT + // + + // Then commit the staged changes, skipping pre-commit hooks + commitCmd := exec.Command("git", "commit", "-m", message, "--no-verify") + commitCmd.Dir = repoDir + + // Set environment variables to ensure pre-commit hooks are skipped + env := os.Environ() + commitCmd.Env = append(env, "SKIP=1", "GIT_SKIP_HOOKS=1") + + commitOutput, commitErr := commitCmd.CombinedOutput() + commitExitCode := procutil.CmdExitCode(commitCmd, commitErr) + if commitExitCode != 0 { + // Exit code 1 with "nothing to commit" message is normal if no changes were staged + if strings.Contains(string(commitOutput), "nothing to commit") { + return nil // No changes to commit, not an error + } + return fmt.Errorf("git commit failed: %v\n%s", commitErr, string(commitOutput)) + } + + // + // PUSH + // + + pushCmd := exec.Command("git", "push", "origin", "HEAD") + pushCmd.Dir = repoDir + + pushOutput, err := pushCmd.CombinedOutput() + exitCode := procutil.CmdExitCode(pushCmd, err) + if exitCode != 0 { + return fmt.Errorf("git push failed: %v\n%s", err, string(pushOutput)) + } + + return nil +} diff --git a/language/scala/scala_package.go b/language/scala/scala_package.go index 84b6e277..942aa2c4 100644 --- a/language/scala/scala_package.go +++ b/language/scala/scala_package.go @@ -3,8 +3,6 @@ package scala import ( "fmt" "log" - "path/filepath" - "strings" "github.com/bazelbuild/bazel-gazelle/config" "github.com/bazelbuild/bazel-gazelle/label" @@ -14,8 +12,6 @@ import ( "github.com/bazelbuild/bazel-gazelle/rule" "github.com/rs/zerolog" - sppb "github.com/stackb/scala-gazelle/build/stack/gazelle/scala/parse" - "github.com/stackb/scala-gazelle/pkg/glob" "github.com/stackb/scala-gazelle/pkg/parser" "github.com/stackb/scala-gazelle/pkg/resolver" "github.com/stackb/scala-gazelle/pkg/scalaconfig" @@ -231,53 +227,30 @@ func (s *scalaPackage) GeneratedRules() (rules []*rule.Rule) { return } -// ParseRule implements part of the scalarule.Package interface. -func (s *scalaPackage) ParseRule(r *rule.Rule, attrName string) (scalaRule scalarule.Rule, err error) { - dir := filepath.Join(s.repoRootDir(), s.args.Rel) - - // collect and filter .scala files from the `srcs` attribute. - srcs, err := glob.CollectFilenames(s.args.File, dir, r.Attr(attrName)) - if err != nil { - return nil, err - } - scalaSrcs := make([]string, 0, len(srcs)) - for _, src := range srcs { - if !strings.HasSuffix(src, ".scala") { - continue - } - scalaSrcs = append(scalaSrcs, src) - } - if len(scalaSrcs) == 0 { - err = ErrRuleHasNoSrcs - } - - logger := s.logger.With().Str("kind", r.Kind()).Str("name", r.Name()).Logger() - logger.Debug().Msgf("%d scala files collected from %s", len(scalaSrcs), attrName) - - from := s.cfg.MaybeRewrite(r.Kind(), label.Label{Pkg: s.args.Rel, Name: r.Name()}) - - rule := &sppb.Rule{ - Label: from.String(), - Kind: r.Kind(), - } - if len(scalaSrcs) > 0 { - rule, err = s.parser.ParseScalaRule(r.Kind(), from, dir, scalaSrcs...) - if err != nil { - logger.Warn().Err(err).Msg("parse error") - return nil, err - } - } +// NewScalaRule implements part of the scalarule.Package interface. +func (s *scalaPackage) NewScalaRule(r *rule.Rule) (scalarule.Rule, error) { ctx := &scalaRuleContext{ + repoRoot: s.repoRootDir(), + file: s.args.File, rule: r, scalaConfig: s.cfg, resolver: s.universe, scope: s.universe, + knownScopes: s.universe, + parser: s.parser, } - scalaRule = newScalaRule(logger, ctx, rule) + logger := s.logger.With().Str("kind", r.Kind()).Str("name", r.Name()).Logger() + from := s.cfg.MaybeRewrite(r.Kind(), label.Label{Pkg: s.args.Rel, Name: r.Name()}) + + scalaRule := newScalaRule(logger, ctx, from) - return + if err := scalaRule.ParseSrcs(); err != nil { + return nil, err + } + + return scalaRule, nil } // repoRootDir return the root directory of the repo. @@ -326,6 +299,12 @@ func (p *scalaPackage) infof(format string, args ...any) string { return fmt.Sprintf("INFO ["+p.args.Rel+"]: "+format, args...) } +// OnEnd is a lifecycle hook that gets called when the resolve phase has +// ended. +func (p *scalaPackage) OnEnd() error { + return nil +} + func ruleContributesToCoverage(name string) bool { switch name { case "scala_files": diff --git a/language/scala/scala_package_test.go b/language/scala/scala_package_test.go index d9d30ba8..c43cec58 100644 --- a/language/scala/scala_package_test.go +++ b/language/scala/scala_package_test.go @@ -15,10 +15,9 @@ import ( "github.com/stackb/scala-gazelle/pkg/scalaconfig" ) -func TestScalaPackageParseRule(t *testing.T) { +func TestScalaPackageNewScalaRule(t *testing.T) { for name, tc := range map[string]struct { rule *rule.Rule - attrName string wantFiles []*sppb.File wantErr string }{ @@ -50,7 +49,7 @@ func TestScalaPackageParseRule(t *testing.T) { } var gotErr string - got, gotError := pkg.ParseRule(tc.rule, tc.attrName) + got, gotError := pkg.NewScalaRule(tc.rule) if gotError != nil { gotErr = gotError.Error() } @@ -58,7 +57,7 @@ func TestScalaPackageParseRule(t *testing.T) { if diff := cmp.Diff(tc.wantErr, gotErr); diff != "" { t.Errorf("error (-want +got):\n%s", diff) } - if diff := cmp.Diff(tc.wantFiles, got.Files()); diff != "" { + if diff := cmp.Diff(tc.wantFiles, got.Rule().Files); diff != "" { t.Errorf("(-want +got):\n%s", diff) } }) diff --git a/language/scala/scala_rule.go b/language/scala/scala_rule.go index 644e0215..1a62601c 100644 --- a/language/scala/scala_rule.go +++ b/language/scala/scala_rule.go @@ -16,6 +16,8 @@ import ( "github.com/stackb/scala-gazelle/pkg/bazel" "github.com/stackb/scala-gazelle/pkg/collections" + "github.com/stackb/scala-gazelle/pkg/glob" + "github.com/stackb/scala-gazelle/pkg/parser" "github.com/stackb/scala-gazelle/pkg/procutil" "github.com/stackb/scala-gazelle/pkg/resolver" "github.com/stackb/scala-gazelle/pkg/scalaconfig" @@ -33,14 +35,21 @@ const ( ) type scalaRuleContext struct { + repoRoot string // the parent config scalaConfig *scalaconfig.Config // rule (lowercase) is the parent gazelle rule rule *rule.Rule + // file to which the rule belongs + file *rule.File // scope is a map of symbols that are outside the rule. scope resolver.Scope // the global import resolver resolver resolver.SymbolResolver + // the global scope registry + knownScopes resolver.KnownScopeRegistry + // instance of the scala rule parser + parser parser.Parser } type scalaRule struct { @@ -48,10 +57,10 @@ type scalaRule struct { logger zerolog.Logger // Rule is the pb representation pb *sppb.Rule - // files is a list of files, copied from pb.Files but sorted again - files []*sppb.File // ctx is the rule context ctx *scalaRuleContext + // from is the label for the rule + from label.Label // exports keyed by their import exports map[string]resolve.ImportSpec } @@ -64,32 +73,59 @@ func init() { } } -func newScalaRule( - logger zerolog.Logger, - ctx *scalaRuleContext, - rule *sppb.Rule, -) *scalaRule { - scalaRule := &scalaRule{ +func newScalaRule(logger zerolog.Logger, ctx *scalaRuleContext, from label.Label) *scalaRule { + return &scalaRule{ logger: logger, - pb: rule, - files: rule.Files, ctx: ctx, + from: from, exports: make(map[string]resolve.ImportSpec), + pb: &sppb.Rule{ + Label: from.String(), + Kind: ctx.rule.Kind(), + }, } +} - sort.Slice(scalaRule.files, func(i, j int) bool { - a := scalaRule.files[i] - b := scalaRule.files[j] - return a.Filename < b.Filename - }) +// ParseSrcs implements part of the scalarule.Rule interface +func (r *scalaRule) ParseSrcs() error { + dir := filepath.Join(r.ctx.repoRoot, r.from.Pkg) + + // collect and filter .scala files from the `srcs` attribute. + srcs, err := glob.CollectFilenames(r.ctx.file, dir, r.ctx.rule.Attr("srcs")) + if err != nil { + return err + } + scalaSrcs := make([]string, 0, len(srcs)) + for _, src := range srcs { + if !strings.HasSuffix(src, ".scala") { + continue + } + scalaSrcs = append(scalaSrcs, src) + } + if len(scalaSrcs) == 0 { + err = ErrRuleHasNoSrcs + } + + if len(scalaSrcs) > 0 { + rule, err := r.ctx.parser.ParseScalaRule(r.ctx.rule.Kind(), r.from, dir, scalaSrcs...) + if err != nil { + r.logger.Warn().Err(err).Msg("parse error") + return err + } + // TODO(pcj): use the pb.struct as-is, or just copy over the files? + r.pb.Files = rule.Files + r.pb.Sha256 = rule.Sha256 + } - if !isBinaryRule(ctx.rule.Kind()) { - for _, file := range scalaRule.files { - scalaRule.putExports(file) + r.logger.Debug().Msgf("%d scala files collected", len(scalaSrcs)) + + if !isBinaryRule(r.ctx.rule.Kind()) { + for _, file := range r.pb.Files { + r.putExports(file) } } - return scalaRule + return nil } // ResolveExports performs symbol resolution for exports of the rule. @@ -166,13 +202,13 @@ func (r *scalaRule) Imports(from label.Label) resolver.ImportMap { } // direct - for _, file := range r.files { + for _, file := range r.pb.Files { r.fileImports(imports, file, from) } // semantic add in semantic imports after direct ones to minimize the delta // between running gazelle with and without semanticdb info. - for _, file := range r.files { + for _, file := range r.pb.Files { r.fileSemanticImports(imports, file, from) } @@ -200,7 +236,7 @@ func (r *scalaRule) Imports(from label.Label) resolver.ImportMap { func (r *scalaRule) Exports(from label.Label) resolver.ImportMap { exports := resolver.NewImportMap() - for _, file := range r.files { + for _, file := range r.pb.Files { r.fileExports(file, exports, from) } @@ -348,6 +384,9 @@ func (r *scalaRule) fileImports(imports resolver.ImportMap, file *sppb.File, fro r.logger.Print(r.infof("%s scope:\n%s", file.Filename, scope.String())) } + // register the scope under workspace-relative path + r.ctx.knownScopes.PutKnownScope(file.Filename, scope) + // resolve extends clauses in the file. While these are probably duplicated // in the 'Names' slice, do it anyway. tokens := extendsKeysSorted(file.Extends) @@ -388,7 +427,12 @@ func (r *scalaRule) fileImports(imports resolver.ImportMap, file *sppb.File, fro // Files implements part of the scalarule.Rule interface. func (r *scalaRule) Files() []*sppb.File { - return r.files + return r.pb.Files +} + +// Rule implements part of the scalarule.Rule interface. +func (r *scalaRule) Rule() *sppb.Rule { + return r.pb } // Provides implements part of the scalarule.Rule interface. diff --git a/language/scala/scala_rule_test.go b/language/scala/scala_rule_test.go index bad992c9..b60ff292 100644 --- a/language/scala/scala_rule_test.go +++ b/language/scala/scala_rule_test.go @@ -80,13 +80,11 @@ func TestScalaRuleExports(t *testing.T) { scalaConfig: sc, resolver: universe, scope: universe, + knownScopes: universe, } - scalaRule := newScalaRule(zerolog.New(os.Stderr), ctx, &sppb.Rule{ - Label: tc.from.String(), - Kind: tc.rule.Kind(), - Files: tc.files, - }) + scalaRule := newScalaRule(zerolog.New(os.Stderr), ctx, tc.from) + scalaRule.Rule().Files = tc.files got := scalaRule.Provides() @@ -276,11 +274,8 @@ func TestScalaRuleImports(t *testing.T) { scope: universe, } - scalaRule := newScalaRule(zerolog.New(os.Stderr), ctx, &sppb.Rule{ - Label: tc.from.String(), - Kind: tc.rule.Kind(), - Files: tc.files, - }) + scalaRule := newScalaRule(zerolog.New(os.Stderr), ctx, tc.from) + scalaRule.Rule().Files = tc.files imports := scalaRule.Imports(tc.from) got := make([]string, len(imports.Keys())) diff --git a/pkg/autokeep/BUILD.bazel b/pkg/autokeep/BUILD.bazel index 33e65838..8694420b 100644 --- a/pkg/autokeep/BUILD.bazel +++ b/pkg/autokeep/BUILD.bazel @@ -12,7 +12,9 @@ go_library( deps = [ "//build/stack/gazelle/scala/autokeep", "//build/stack/gazelle/scala/cache", + "//build/stack/gazelle/scala/parse", "//pkg/protobuf", + "//pkg/resolver", "@bazel_gazelle//label:go_default_library", "@bazel_gazelle//rule:go_default_library", "@com_github_bazelbuild_buildtools//build:go_default_library", diff --git a/pkg/autokeep/deps.go b/pkg/autokeep/deps.go index abf92445..d6812c2e 100644 --- a/pkg/autokeep/deps.go +++ b/pkg/autokeep/deps.go @@ -13,13 +13,16 @@ import ( "github.com/bazelbuild/buildtools/build" akpb "github.com/stackb/scala-gazelle/build/stack/gazelle/scala/autokeep" scpb "github.com/stackb/scala-gazelle/build/stack/gazelle/scala/cache" + sppb "github.com/stackb/scala-gazelle/build/stack/gazelle/scala/parse" "github.com/stackb/scala-gazelle/pkg/protobuf" + "github.com/stackb/scala-gazelle/pkg/resolver" ) type DepsMap map[string]string +type FileMap map[string]*sppb.File -func MakeDeltaDeps(deps DepsMap, diagnostics *akpb.Diagnostics) *akpb.DeltaDeps { +func MakeDeltaDeps(diagnostics *akpb.Diagnostics, deps DepsMap, files FileMap, scopes resolver.KnownScopeRegistry, globalScope resolver.Scope) *akpb.DeltaDeps { rules := make(map[string]*akpb.RuleDeps) delta := new(akpb.DeltaDeps) for _, e := range diagnostics.ScalacErrors { @@ -29,7 +32,32 @@ func MakeDeltaDeps(deps DepsMap, diagnostics *akpb.Diagnostics) *akpb.DeltaDeps rule.Label = e.RuleLabel rule.BuildFile = e.BuildFile } + + lookup := func(sourceFile string, name string) (*resolver.Symbol, bool) { + scope, ok := scopes.GetKnownScope(sourceFile) + if !ok { + return nil, false + } + sym, ok := scope.GetSymbol(name) + if !ok { + return nil, false + } + return sym, true + } + switch t := e.Error.(type) { + case *akpb.ScalacError_NotFound: + if sym, ok := lookup(t.NotFound.SourceFile, t.NotFound.Type); ok { + if sym.Label != label.NoLabel { + log.Println("MATCH: ", sym, "satisfies not found type", t.NotFound.Type) + if len(rule.Deps) == 0 { + delta.Add = append(delta.Add, rule) + } + rule.Deps = append(rule.Deps, sym.Label.String()) + } + } else { + log.Println("MISS (not found):", t.NotFound.SourceFile, t.NotFound.Type) + } case *akpb.ScalacError_MissingSymbol: sym := t.MissingSymbol.Symbol if label, ok := deps[sym]; ok { @@ -42,16 +70,25 @@ func MakeDeltaDeps(deps DepsMap, diagnostics *akpb.Diagnostics) *akpb.DeltaDeps log.Printf("MISS (missing symbol not found): %q", sym) } case *akpb.ScalacError_NotAMemberOfPackage: - sym := fmt.Sprintf("%s.%s", t.NotAMemberOfPackage.PackageName, t.NotAMemberOfPackage.Symbol) - if label, ok := deps[sym]; ok { - log.Println("MATCH: ", sym, "is provided by", label) - if len(rule.Deps) == 0 { - delta.Add = append(delta.Add, rule) + name := fmt.Sprintf("%s.%s", t.NotAMemberOfPackage.PackageName, t.NotAMemberOfPackage.Symbol) + if sym, ok := lookup(t.NotAMemberOfPackage.SourceFile, name); ok { + if sym.Label != label.NoLabel { + log.Println("MATCH: ", sym, "satisfies not a member of package type", name) + if len(rule.Deps) == 0 { + delta.Add = append(delta.Add, rule) + } + rule.Deps = append(rule.Deps, sym.Label.String()) } - rule.Deps = append(rule.Deps, label) - } else { - log.Printf("MISS (not found): %q (%d)", sym, len(deps)) } + // if label, ok := deps[sym]; ok { + // log.Println("MATCH: ", sym, "is provided by", label) + // if len(rule.Deps) == 0 { + // delta.Add = append(delta.Add, rule) + // } + // rule.Deps = append(rule.Deps, label) + // } else { + // log.Printf("MISS (not found): %q (%d)", sym, len(deps)) + // } case *akpb.ScalacError_BuildozerUnusedDep: delta.Remove = append(delta.Remove, rule) rule.Deps = append(rule.Deps, t.BuildozerUnusedDep.UnusedDep) diff --git a/pkg/autokeep/deps_test.go b/pkg/autokeep/deps_test.go index 2e7304e2..3b8af948 100644 --- a/pkg/autokeep/deps_test.go +++ b/pkg/autokeep/deps_test.go @@ -8,25 +8,26 @@ import ( "github.com/bazelbuild/bazel-gazelle/testtools" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" - akpb "github.com/stackb/scala-gazelle/build/stack/gazelle/scala/autokeep" "github.com/stackb/scala-gazelle/pkg/testutil" + + akpb "github.com/stackb/scala-gazelle/build/stack/gazelle/scala/autokeep" ) func TestMakeDeltaDeps(t *testing.T) { for name, tc := range map[string]struct { - input *akpb.Diagnostics - deps DepsMap - want *akpb.DeltaDeps + diagnostics *akpb.Diagnostics + deps DepsMap + want *akpb.DeltaDeps }{ "degenerate": { - input: &akpb.Diagnostics{}, - want: &akpb.DeltaDeps{}, + diagnostics: &akpb.Diagnostics{}, + want: &akpb.DeltaDeps{}, }, "not-a-package": { deps: map[string]string{ "contoso.postswarm.SelectiveSpotSessionUtils": "//contoso/postswarm:selective_spot_session_utils_common_scala", }, - input: &akpb.Diagnostics{ + diagnostics: &akpb.Diagnostics{ ScalacErrors: []*akpb.ScalacError{ { RuleLabel: "//contoso/postswarm:grey_it", @@ -52,7 +53,7 @@ func TestMakeDeltaDeps(t *testing.T) { }, } { t.Run(name, func(t *testing.T) { - got := MakeDeltaDeps(tc.deps, tc.input) + got := MakeDeltaDeps(tc.diagnostics, tc.deps, nil, nil, nil) if diff := cmp.Diff(tc.want, got, cmpopts.IgnoreUnexported( akpb.DeltaDeps{}, diff --git a/pkg/autokeep/scanner.go b/pkg/autokeep/scanner.go index 98404f96..1a935f29 100644 --- a/pkg/autokeep/scanner.go +++ b/pkg/autokeep/scanner.go @@ -20,6 +20,9 @@ var missingSymbolLine = regexp.MustCompile(`^(.*):\d+: error: Symbol 'type ([^'] // omnistac/postswarm/src/it/scala/omnistac/postswarm/grey/SelectiveSpottingTest.scala:22: error: [rewritten by -quickfix] object SelectiveSpotSessionUtils is not a member of package omnistac.postswarm var notAMemberOfPackageLine = regexp.MustCompile(`^(.*):\d+: error: .* object ([A-Z][_a-zA-Z0-9]*) is not a member of package (.*)$`) +// trumid/fix/common/testing/src/QuickfixTestUtils.scala:88: error: [rewritten by -quickfix] not found: type SimpleQuickfixSessionObject +var typeNotFoundLine = regexp.MustCompile(`^(.*):\d+: error: .* not found: type ([A-Z][_a-zA-Z0-9]*)$`) + // This symbol is required by 'class omnistac.gum.dao.TradingAccountDao.TradingAccountTable'. var symbolRequiredByLine = regexp.MustCompile(`^This symbol is required by '([^']+)'.$`) @@ -28,8 +31,18 @@ var buildozerLine = regexp.MustCompile(`^buildozer 'remove deps ([^']+)' (.*)$`) func ScanOutput(output []byte) (*akpb.Diagnostics, error) { diagnostics := new(akpb.Diagnostics) + + // scalacError is populated when we hit the first scalacErrorLine and + // becomes a contextual object upon which the state of future errors (lines + // following) depends. var scalacError *akpb.ScalacError - var missingSymbol *akpb.MissingSymbol + + addError := func(sce *akpb.ScalacError) { + sce.BuildFile = scalacError.BuildFile + sce.RuleLabel = scalacError.RuleLabel + diagnostics.ScalacErrors = append(diagnostics.ScalacErrors, sce) + } + scanner := bufio.NewScanner(bytes.NewReader(output)) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) @@ -42,31 +55,50 @@ func ScanOutput(output []byte) (*akpb.Diagnostics, error) { scalacError = new(akpb.ScalacError) scalacError.BuildFile = match[1] scalacError.RuleLabel = strings.TrimSuffix(match[2], "_testlib") - diagnostics.ScalacErrors = append(diagnostics.ScalacErrors, scalacError) - missingSymbol = nil } else if match := symbolRequiredByLine.FindStringSubmatch(line); match != nil { - missingSymbol.RequiredBy = match[1] + if len(diagnostics.ScalacErrors) > 0 { + lastError := diagnostics.ScalacErrors[len(diagnostics.ScalacErrors)-1] + if e, ok := lastError.Error.(*akpb.ScalacError_MissingSymbol); ok { + e.MissingSymbol.RequiredBy = match[1] + } + } } else if match := missingSymbolLine.FindStringSubmatch(line); match != nil { - scalacError.Error = &akpb.ScalacError_MissingSymbol{ - MissingSymbol: &akpb.MissingSymbol{ - SourceFile: match[1], - Symbol: match[2], + addError(&akpb.ScalacError{ + Error: &akpb.ScalacError_MissingSymbol{ + MissingSymbol: &akpb.MissingSymbol{ + SourceFile: match[1], + Symbol: match[2], + }, }, - } + }) + } else if match := typeNotFoundLine.FindStringSubmatch(line); match != nil { + addError(&akpb.ScalacError{ + Error: &akpb.ScalacError_NotFound{ + NotFound: &akpb.TypeNotFound{ + SourceFile: match[1], + Type: match[2], + }, + }, + }) } else if match := notAMemberOfPackageLine.FindStringSubmatch(line); match != nil { - scalacError.Error = &akpb.ScalacError_NotAMemberOfPackage{ - NotAMemberOfPackage: &akpb.NotAMemberOfPackage{ - Symbol: match[2], - PackageName: match[3], + addError(&akpb.ScalacError{ + Error: &akpb.ScalacError_NotAMemberOfPackage{ + NotAMemberOfPackage: &akpb.NotAMemberOfPackage{ + SourceFile: match[1], + Symbol: match[2], + PackageName: match[3], + }, }, - } + }) } else if match := buildozerLine.FindStringSubmatch(line); match != nil { - scalacError.Error = &akpb.ScalacError_BuildozerUnusedDep{ - BuildozerUnusedDep: &akpb.BuildozerUnusedDep{ - UnusedDep: match[1], - RuleLabel: match[2], + addError(&akpb.ScalacError{ + Error: &akpb.ScalacError_BuildozerUnusedDep{ + BuildozerUnusedDep: &akpb.BuildozerUnusedDep{ + UnusedDep: match[1], + RuleLabel: match[2], + }, }, - } + }) } } if err := scanner.Err(); err != nil { diff --git a/pkg/autokeep/scanner_test.go b/pkg/autokeep/scanner_test.go index c99abb30..29e6be65 100644 --- a/pkg/autokeep/scanner_test.go +++ b/pkg/autokeep/scanner_test.go @@ -43,6 +43,7 @@ omnistac/postswarm/src/it/scala/omnistac/postswarm/grey/SelectiveSpottingTest.sc BuildFile: "/Users/pcj/go/src/github.com/Omnistac/unity/omnistac/postswarm/BUILD.bazel", Error: &akpb.ScalacError_NotAMemberOfPackage{ NotAMemberOfPackage: &akpb.NotAMemberOfPackage{ + SourceFile: "omnistac/postswarm/src/it/scala/omnistac/postswarm/grey/SelectiveSpottingTest.scala", Symbol: "SelectiveSpotSessionUtils", PackageName: "omnistac.postswarm", }, diff --git a/pkg/collections/BUILD.bazel b/pkg/collections/BUILD.bazel index 1efb27c2..a8a63161 100644 --- a/pkg/collections/BUILD.bazel +++ b/pkg/collections/BUILD.bazel @@ -14,6 +14,7 @@ go_library( "slice.go", "string_slice.go", "string_stack.go", + "terminal.go", "uint32stack.go", ], importpath = "github.com/stackb/scala-gazelle/pkg/collections", @@ -35,6 +36,7 @@ package_filegroup( "slice.go", "string_slice.go", "string_stack.go", + "terminal.go", "uint32stack.go", ], visibility = ["//visibility:public"], diff --git a/pkg/collections/slice.go b/pkg/collections/slice.go index 4c05cf3c..dfe2d6a9 100644 --- a/pkg/collections/slice.go +++ b/pkg/collections/slice.go @@ -26,3 +26,19 @@ func SliceInsertAt[T any](slice []T, i int, value T) []T { return result } + +// SliceDeduplicate removes duplicate entries +func SliceDeduplicate[T any](in []T) (out []T) { + if len(in) == 0 { + return in + } + seen := make(map[any]bool) + for _, v := range in { + if seen[v] { + continue + } + seen[v] = true + out = append(out, v) + } + return +} diff --git a/pkg/collections/terminal.go b/pkg/collections/terminal.go new file mode 100644 index 00000000..0224625c --- /dev/null +++ b/pkg/collections/terminal.go @@ -0,0 +1 @@ +package collections diff --git a/pkg/provider/source_provider.go b/pkg/provider/source_provider.go index e7bf0e58..e14354b7 100644 --- a/pkg/provider/source_provider.go +++ b/pkg/provider/source_provider.go @@ -15,6 +15,7 @@ import ( "github.com/bazelbuild/buildtools/build" "github.com/rs/zerolog" + "github.com/stackb/scala-gazelle/pkg/collections" "github.com/stackb/scala-gazelle/pkg/parser" "github.com/stackb/scala-gazelle/pkg/procutil" "github.com/stackb/scala-gazelle/pkg/protobuf" @@ -58,6 +59,14 @@ type SourceProvider struct { scalaFilesetFilename string // scalaFiles is a mapping that is read from the scalaFilesetFilename, if present scalaFiles map[string]*sppb.File + // hasLifecycleEnded flags whether we have seen the OnEnd() call at least + // once. This affects whether we choose to double-check sha256 values + // during the haveFiles check. For the first go, assume all files we + // already have are up-to-date (don't check sha256's again). If it happens + // late, assume we are in watch/repair mode and the gazelle process is + // long-lived, possibly needing to reparse files that could be changed by a + // developer since the gazelle process strated. + hasLifecycleEnded bool } // Name implements part of the resolver.SymbolProvider interface. @@ -97,6 +106,7 @@ func (r *SourceProvider) OnResolve() error { // OnEnd implements part of the resolver.SymbolProvider interface. func (r *SourceProvider) OnEnd() error { + r.hasLifecycleEnded = true return nil } @@ -166,20 +176,41 @@ func (r *SourceProvider) ParseScalaRule(kind string, from label.Label, dir strin } func (r *SourceProvider) parseFiles(dir string, srcs []string, from label.Label, kind string) ([]*sppb.File, error) { - // haveFiles is the list of haveFiles we already have from pre-computed scalaFiles + // haveFiles is the list of haveFiles we already have from pre-computed + // scalaFiles whose sha256 values are up-to-date. var haveFiles []*sppb.File - // needFilenames is a mapping from the absolute to relative path + // needFilenames is a mapping from the absolute to relative path of the files we need to parse needFilenames := make(map[string]string) for _, src := range srcs { + abs := filepath.Join(dir, src) rel := filepath.Join(from.Pkg, src) if file, ok := r.scalaFiles[rel]; ok { - haveFiles = append(haveFiles, file) - // log.Println("✅ have:", rel) - } else { - abs := filepath.Join(dir, src) - needFilenames[abs] = rel + var isUpToDate bool + + if r.hasLifecycleEnded { + // this is a later pass during gazelle execution such as in + // watch/repair mode, we need to re-check sha256s + sha256, err := collections.FileSha256(abs) + if err != nil { + return nil, err + } + if sha256 == file.Sha256 { + isUpToDate = true + } + } else { + // this is the (normal) first pass during gazelle execution, no + // need to re-check sha256s + isUpToDate = true + } + + if isUpToDate { + haveFiles = append(haveFiles, file) + // log.Println("✅ have:", rel) + continue + } } + needFilenames[abs] = rel } if len(needFilenames) == 0 { return haveFiles, nil @@ -225,6 +256,8 @@ func (r *SourceProvider) parseFiles(dir string, srcs []string, from label.Label, panic("failed to map parsed file (having absolute path) back to relative path: this is a bug: " + file.Filename) } file.Filename = rel + // update saved cache + r.scalaFiles[rel] = file } return append(haveFiles, response.Files...), nil diff --git a/pkg/resolver/BUILD.bazel b/pkg/resolver/BUILD.bazel index d7f69e5a..8b37b7d8 100644 --- a/pkg/resolver/BUILD.bazel +++ b/pkg/resolver/BUILD.bazel @@ -17,6 +17,7 @@ go_library( "import.go", "import_map.go", "known_rule_registry.go", + "known_scope_registry.go", "label_name_rewrite_spec.go", "memo_symbol_resolver.go", "override_symbol_resolver.go", @@ -101,6 +102,7 @@ package_filegroup( "import_map.go", "import_map_test.go", "known_rule_registry.go", + "known_scope_registry.go", "label_name_rewrite_spec.go", "memo_symbol_resolver.go", "override_symbol_resolver.go", diff --git a/pkg/resolver/import_map.go b/pkg/resolver/import_map.go index 13b9dc53..f98f1b97 100644 --- a/pkg/resolver/import_map.go +++ b/pkg/resolver/import_map.go @@ -2,7 +2,6 @@ package resolver import ( "github.com/bazelbuild/bazel-gazelle/label" - "github.com/bazelbuild/buildtools/build" ) // ImportLabel is a pair of (Import,Label) @@ -11,16 +10,17 @@ type ImportLabel struct { Label label.Label } +// ImportMap is a map-like entity that is capable of producing a list of +// dependencies relative to a Label. type ImportMap interface { Keys() []string Values() []*Import Deps(from label.Label) map[label.Label]*ImportLabel Put(imp *Import) Get(name string) (*Import, bool) - Annotate(comments *build.Comments, accept func(imp *Import) bool) } -// OrderedImportMap is a map if imports keyed by the import string. +// OrderedImportMap is a map of imports keyed by the import string. type OrderedImportMap struct { values []*Import has map[string]bool @@ -108,12 +108,3 @@ func (imports *OrderedImportMap) Put(imp *Import) { imports.has[imp.Imp] = true imports.values = append(imports.values, imp) } - -func (imports *OrderedImportMap) Annotate(comments *build.Comments, accept func(imp *Import) bool) { - for _, imp := range imports.values { - if !accept(imp) { - continue - } - comments.Before = append(comments.Before, build.Comment{Token: "# " + imp.String()}) - } -} diff --git a/pkg/resolver/known_scope_registry.go b/pkg/resolver/known_scope_registry.go new file mode 100644 index 00000000..b84a0a90 --- /dev/null +++ b/pkg/resolver/known_scope_registry.go @@ -0,0 +1,17 @@ +package resolver + +// KnownScopeRegistry is an index of scopes keyed by their filename. For +// example, if one needed to resolve the symbol 'A' within file +// `a/b/c/Main.scala`, the registry could be used to gain the scope to perform +// this lookup. +type KnownScopeRegistry interface { + // GetScope does a lookup of the given label and returns the + // known rule. If not known `(nil, false)` is returned. + GetKnownScope(name string) (Scope, bool) + + // PutFileScope adds the given known rule to the registry. It is an + // error to attempt duplicate registration of the same rule twice. + // Implementations should use the google.golang.org/grpc/status.Errorf for + // error types. + PutKnownScope(name string, scope Scope) error +} diff --git a/pkg/resolver/mocks/BUILD.bazel b/pkg/resolver/mocks/BUILD.bazel index 04a6ed98..3b4c4af0 100644 --- a/pkg/resolver/mocks/BUILD.bazel +++ b/pkg/resolver/mocks/BUILD.bazel @@ -21,7 +21,6 @@ go_library( "@bazel_gazelle//resolve:go_default_library", "@bazel_gazelle//rule:go_default_library", "@com_github_bazelbuild_buildtools//build:go_default_library", - "@com_github_rs_zerolog//:zerolog", "@com_github_stretchr_testify//mock", ], ) diff --git a/pkg/resolver/mocks/Universe.go b/pkg/resolver/mocks/Universe.go index 0d0d4014..d33e147e 100644 --- a/pkg/resolver/mocks/Universe.go +++ b/pkg/resolver/mocks/Universe.go @@ -128,6 +128,36 @@ func (_m *Universe) GetKnownRule(from label.Label) (*rule.Rule, bool) { return r0, r1 } +// GetKnownScope provides a mock function with given fields: name +func (_m *Universe) GetKnownScope(name string) (resolver.Scope, bool) { + ret := _m.Called(name) + + if len(ret) == 0 { + panic("no return value specified for GetKnownScope") + } + + var r0 resolver.Scope + var r1 bool + if rf, ok := ret.Get(0).(func(string) (resolver.Scope, bool)); ok { + return rf(name) + } + if rf, ok := ret.Get(0).(func(string) resolver.Scope); ok { + r0 = rf(name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(resolver.Scope) + } + } + + if rf, ok := ret.Get(1).(func(string) bool); ok { + r1 = rf(name) + } else { + r1 = ret.Get(1).(bool) + } + + return r0, r1 +} + // GetScope provides a mock function with given fields: name func (_m *Universe) GetScope(name string) (resolver.Scope, bool) { ret := _m.Called(name) @@ -262,6 +292,24 @@ func (_m *Universe) PutKnownRule(from label.Label, r *rule.Rule) error { return r0 } +// PutKnownScope provides a mock function with given fields: name, scope +func (_m *Universe) PutKnownScope(name string, scope resolver.Scope) error { + ret := _m.Called(name, scope) + + if len(ret) == 0 { + panic("no return value specified for PutKnownScope") + } + + var r0 error + if rf, ok := ret.Get(0).(func(string, resolver.Scope) error); ok { + r0 = rf(name, scope) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // PutSymbol provides a mock function with given fields: known func (_m *Universe) PutSymbol(known *resolver.Symbol) error { ret := _m.Called(known) diff --git a/pkg/resolver/trie_scope_test.go b/pkg/resolver/trie_scope_test.go index 1a8e1346..46785bf1 100644 --- a/pkg/resolver/trie_scope_test.go +++ b/pkg/resolver/trie_scope_test.go @@ -15,7 +15,7 @@ func makeSymbol(typ sppb.ImportType, name string, from label.Label) *Symbol { return &Symbol{ Type: typ, Name: name, - Label: label.NoLabel, + Label: from, Provider: "test", } } diff --git a/pkg/resolver/universe.go b/pkg/resolver/universe.go index af73a58f..3f3dcce9 100644 --- a/pkg/resolver/universe.go +++ b/pkg/resolver/universe.go @@ -5,6 +5,7 @@ package resolver type Universe interface { SymbolProviderRegistry KnownRuleRegistry + KnownScopeRegistry ConflictResolverRegistry DepsCleanerRegistry Scope diff --git a/pkg/scalaconfig/BUILD.bazel b/pkg/scalaconfig/BUILD.bazel index 1a44329d..f053dad4 100644 --- a/pkg/scalaconfig/BUILD.bazel +++ b/pkg/scalaconfig/BUILD.bazel @@ -10,6 +10,7 @@ go_library( "//pkg/collections", "//pkg/resolver", "//pkg/scalarule", + "//pkg/sweep", "@bazel_gazelle//config:go_default_library", "@bazel_gazelle//label:go_default_library", "@bazel_gazelle//resolve:go_default_library", diff --git a/pkg/scalaconfig/config.go b/pkg/scalaconfig/config.go index 5f0d7e0b..d14add12 100644 --- a/pkg/scalaconfig/config.go +++ b/pkg/scalaconfig/config.go @@ -18,13 +18,14 @@ import ( "github.com/stackb/scala-gazelle/pkg/collections" "github.com/stackb/scala-gazelle/pkg/resolver" "github.com/stackb/scala-gazelle/pkg/scalarule" + "github.com/stackb/scala-gazelle/pkg/sweep" ) type debugAnnotation int const ( - scalaLangName = "scala" - TransitiveCommentToken = "# TRANSITIVE" + // scalaLangName is the scala language name + scalaLangName = "scala" ) const ( @@ -59,17 +60,6 @@ const ( // gazelle:scala_fix_wildcard_imports .scala examples.aeron.api.proto._ scalaFixWildcardImportDirective = "scala_fix_wildcard_imports" - // Flag to preserve deps if the label is not known to be needed from the - // imports (legacy migration mode). - // - // gazelle:scala_keep_unknown_deps true - scalaKeepUnknownDepsDirective = "scala_keep_unknown_deps" - - // Turn on the dep sweeper - // - // gazelle:scala_sweep_transitive_deps true - scalaSweepTransitiveDepsDirective = "scala_sweep_transitive_deps" - // Configure a scala rule // // gazelle:scala_rule RULE_NAME ATTRIBUTE VALUE @@ -147,8 +137,10 @@ func DirectiveNames() []string { scalaDebugDirective, scalaLogLevelDirective, scalaDepsCleanerDirective, - scalaKeepUnknownDepsDirective, - scalaSweepTransitiveDepsDirective, + sweep.ScalaKeepUnknownDepsDirective, + sweep.ScalaSweepTransitiveDepsDirective, + sweep.ScalaRepairTransitiveDepsDirective, + sweep.ScalaFixDepsDirective, scalaFixWildcardImportDirective, scalaGenerateBuildFilesDirective, scalaRuleDirective, @@ -172,6 +164,8 @@ type Config struct { generateBuildFiles bool keepUnknownDeps bool sweepTransitiveDeps bool + repairTransitiveDeps bool + fixDeps bool logger zerolog.Logger logLevel zerolog.Level } @@ -220,6 +214,8 @@ func (c *Config) clone(config *config.Config, rel string) *Config { clone.generateBuildFiles = c.generateBuildFiles clone.keepUnknownDeps = c.keepUnknownDeps clone.sweepTransitiveDeps = c.sweepTransitiveDeps + clone.repairTransitiveDeps = c.repairTransitiveDeps + clone.fixDeps = c.fixDeps for k, v := range c.annotations { clone.annotations[k] = v @@ -305,13 +301,29 @@ func (c *Config) ParseDirectives(directives []rule.Directive) error { if err := c.parseScalaRuleDirective(d); err != nil { return fmt.Errorf(`invalid directive: "gazelle:%s %s": %w`, d.Key, d.Value, err) } - case scalaKeepUnknownDepsDirective: - if err := c.parseKeepUnknownDepsDirective(d); err != nil { + case sweep.ScalaKeepUnknownDepsDirective: + if value, err := parseBoolDirective(d); err != nil { + return err + } else { + c.keepUnknownDeps = value + } + case sweep.ScalaFixDepsDirective: + if value, err := parseBoolDirective(d); err != nil { return err + } else { + c.fixDeps = value } - case scalaSweepTransitiveDepsDirective: - if err := c.parseSweepTransitiveDepsDirective(d); err != nil { + case sweep.ScalaSweepTransitiveDepsDirective: + if value, err := parseBoolDirective(d); err != nil { return err + } else { + c.sweepTransitiveDeps = value + } + case sweep.ScalaRepairTransitiveDepsDirective: + if value, err := parseBoolDirective(d); err != nil { + return err + } else { + c.repairTransitiveDeps = value } case scalaFixWildcardImportDirective: c.parseFixWildcardImport(d) @@ -417,32 +429,6 @@ func (c *Config) parseFixWildcardImport(d rule.Directive) { } } -func (c *Config) parseKeepUnknownDepsDirective(d rule.Directive) error { - parts := strings.Fields(d.Value) - if len(parts) != 1 { - return fmt.Errorf("invalid gazelle:%s directive: expected [true|false], got %v", scalaKeepUnknownDepsDirective, parts) - } - keepUnknownDeps, err := strconv.ParseBool(parts[0]) - if err != nil { - return fmt.Errorf("invalid gazelle:%s directive: %v", scalaKeepUnknownDepsDirective, err) - } - c.keepUnknownDeps = keepUnknownDeps - return nil -} - -func (c *Config) parseSweepTransitiveDepsDirective(d rule.Directive) error { - parts := strings.Fields(d.Value) - if len(parts) != 1 { - return fmt.Errorf("invalid gazelle:%s directive: expected [true|false], got %v", scalaSweepTransitiveDepsDirective, parts) - } - sweepTransitiveDeps, err := strconv.ParseBool(parts[0]) - if err != nil { - return fmt.Errorf("invalid gazelle:%s directive: %v", scalaSweepTransitiveDepsDirective, err) - } - c.sweepTransitiveDeps = sweepTransitiveDeps - return nil -} - func (c *Config) parseResolveFileSymbolNames(d rule.Directive) { parts := strings.Fields(d.Value) if len(parts) < 2 { @@ -632,19 +618,33 @@ func (c *Config) depSuffixComment(imp *resolver.Import) *build.Comment { // ShouldSweepTransitive determines whether non-managed deps (not generated, and // not marked with # keep) in the current package should be kept. -func (c *Config) ShouldSweepTransitive(attrName string) bool { +func (c *Config) ShouldSweepTransitive(r *rule.Rule, attrName string) bool { if attrName != "deps" { return false } + if sweep.HasSweepTransitiveDepsTag(r) { + return true + } return c.sweepTransitiveDeps } +// ShouldFixDeps determines whether dep fixer should be applied. +func (c *Config) ShouldFixDeps() bool { + return c.fixDeps +} + // shouldKeepUnknownDeps determines whether non-managed deps should be // kept. func (c *Config) shouldKeepUnknownDeps() bool { return c.keepUnknownDeps } +// ShouldRepairTransitiveDeps determines whether non-managed deps should be +// kept. +func (c *Config) ShouldRepairTransitiveDeps() bool { + return c.repairTransitiveDeps +} + // ShouldFixWildcardImport tests whether the given symbol name pattern // should be resolved within the scope of the given filename pattern. // resolveFileSymbolNameSpecs represent a whitelist; if no patterns match, false @@ -713,15 +713,8 @@ func (c *Config) MaybeRewrite(kind string, from label.Label) label.Label { return from } -func (c *Config) Imports(imports resolver.ImportMap, r *rule.Rule, attrName string, from label.Label) { - c.ruleAttrMergeDeps(imports, r, attrName, from) -} - -func (c *Config) Exports(exports resolver.ImportMap, r *rule.Rule, attrName string, from label.Label) { - c.ruleAttrMergeDeps(exports, r, attrName, from) -} - -func (c *Config) ruleAttrMergeDeps( +// MergeDepsAttr takes an importMap and the rule, and merges the imports into the attr. +func (c *Config) MergeDepsAttr( imports resolver.ImportMap, r *rule.Rule, attrName string, @@ -742,7 +735,7 @@ func (c *Config) ruleAttrMergeDeps( // Merge the current list against the new incoming ones. If no deps remain, // delete the attr. - next := c.mergeDeps(r.Attr(attrName), deps, labels, attrName, from) + next := c.mergeDeps(deps, labels, attrName, r, from) if len(next.List) > 0 { r.SetAttr(attrName, next) @@ -751,10 +744,12 @@ func (c *Config) ruleAttrMergeDeps( } } -// mergeDeps filters out a `deps` list. Extries are removed from the -// list if they can be parsed as dependency labels that have a provider. Others -// types of expressions are left as-is. -func (c *Config) mergeDeps(attrValue build.Expr, deps map[label.Label]bool, importLabels map[label.Label]*resolver.ImportLabel, attrName string, from label.Label) *build.ListExpr { +// mergeDeps filters out a `deps` list. Extries are removed from the list if +// they can be parsed as dependency labels that have a provider. Others types +// of expressions are left as-is. +func (c *Config) mergeDeps(deps map[label.Label]bool, importLabels map[label.Label]*resolver.ImportLabel, attrName string, r *rule.Rule, from label.Label) *build.ListExpr { + attrValue := r.Attr(attrName) + var src *build.ListExpr if attrValue != nil { if current, ok := attrValue.(*build.ListExpr); ok { @@ -766,6 +761,8 @@ func (c *Config) mergeDeps(attrValue build.Expr, deps map[label.Label]bool, impo src = new(build.ListExpr) } + var unknown []string + var dst = new(build.ListExpr) for _, expr := range src.List { // try and parse the expression as a label @@ -802,14 +799,15 @@ func (c *Config) mergeDeps(attrValue build.Expr, deps map[label.Label]bool, impo // are in sweep mode, mark it, keep it and it will be checked by the // sweeper process. Otherwise, only keep it if has already been // marked as TRANSITIVE. - if c.ShouldSweepTransitive(attrName) { + if c.ShouldSweepTransitive(r, attrName) { // set as TRANSITIVE comment for sweeping - if _, ok := expr.(*build.StringExpr); ok { - expr.Comment().Suffix = []build.Comment{{Token: TransitiveCommentToken}} - } + // if _, ok := expr.(*build.StringExpr); ok { + // expr.Comment().Suffix = []build.Comment{sweep.MakeTransitiveComment()} + // } + unknown = append(unknown, dep.String()) dst.List = append(dst.List, expr) } else { - if isTransitive(expr) { + if sweep.IsTransitiveDep(expr) { dst.List = append(dst.List, expr) } else { // one more caveat: preserve unmarked deps in legacy mode @@ -821,6 +819,9 @@ func (c *Config) mergeDeps(attrValue build.Expr, deps map[label.Label]bool, impo continue } + // set a private attr so that they can be potentially removed later + r.SetPrivateAttr("_unknown_"+attrName, unknown) + // do we have a known provider for the dependency? If not, this // dependency is not "managed", so delete it. if !c.shouldKeep(expr, imp, from) { @@ -924,6 +925,18 @@ func parseAnnotation(val string) debugAnnotation { } } +func parseBoolDirective(d rule.Directive) (bool, error) { + parts := strings.Fields(d.Value) + if len(parts) != 1 { + return false, fmt.Errorf("invalid gazelle:%s directive: expected [true|false], got %v", d.Key, parts) + } + value, err := strconv.ParseBool(parts[0]) + if err != nil { + return false, fmt.Errorf("invalid gazelle:%s directive: %v", d.Key, err) + } + return value, nil +} + func removeConflictResolver(slice []resolver.ConflictResolver, index int) []resolver.ConflictResolver { return append(slice[:index], slice[index+1:]...) } @@ -956,14 +969,3 @@ func setCommentPrefix(comment build.Comment, prefix string) build.Comment { comment.Token = "# " + prefix + strings.TrimSpace(strings.TrimPrefix(comment.Token, "#")) return comment } - -// isTransitive returns whether e is marked with a "# TRANSITIVE" comment. -func isTransitive(e build.Expr) bool { - for _, c := range e.Comment().Suffix { - text := strings.TrimSpace(c.Token) - if text == TransitiveCommentToken { - return true - } - } - return false -} diff --git a/pkg/scalafiles/BUILD.bazel b/pkg/scalafiles/BUILD.bazel index fae30e49..40b54366 100644 --- a/pkg/scalafiles/BUILD.bazel +++ b/pkg/scalafiles/BUILD.bazel @@ -10,7 +10,6 @@ go_library( importpath = "github.com/stackb/scala-gazelle/pkg/scalafiles", visibility = ["//visibility:public"], deps = [ - "//build/stack/gazelle/scala/parse", "//pkg/collections", "//pkg/scalarule", "@bazel_gazelle//config:go_default_library", diff --git a/pkg/scalafiles/scala_files.go b/pkg/scalafiles/scala_files.go index e0d74c08..b7959033 100644 --- a/pkg/scalafiles/scala_files.go +++ b/pkg/scalafiles/scala_files.go @@ -10,8 +10,6 @@ import ( "github.com/stackb/scala-gazelle/pkg/collections" "github.com/stackb/scala-gazelle/pkg/scalarule" - - sppb "github.com/stackb/scala-gazelle/build/stack/gazelle/scala/parse" ) const ( @@ -132,18 +130,13 @@ func (s *scalaFilesRule) Resolve(rctx *scalarule.ResolveContext, importsRaw inte } for _, rule := range s.pkg.GeneratedRules() { - scalaFiles := rule.PrivateAttr("_scala_files") - if debug { - log.Printf("%s: resolving with scala_files: %T", rctx.From, scalaFiles) + scalaRule, ok := scalarule.GetRule(rule) + if !ok { + continue } - if files, ok := scalaFiles.([]*sppb.File); ok { - if debug { - log.Printf("%s: resolving with %d files", rctx.From, len(files)) - } - for _, file := range files { - relativeToPkg := strings.TrimPrefix(file.Filename, s.pkg.GenerateArgs().Rel) - srcs = append(srcs, strings.TrimPrefix(relativeToPkg, "/")) - } + for _, file := range scalaRule.Rule().Files { + relativeToPkg := strings.TrimPrefix(file.Filename, s.pkg.GenerateArgs().Rel) + srcs = append(srcs, strings.TrimPrefix(relativeToPkg, "/")) } } if debug { diff --git a/pkg/scalarule/config.go b/pkg/scalarule/config.go index 66b8dc2e..348391c4 100644 --- a/pkg/scalarule/config.go +++ b/pkg/scalarule/config.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/bazelbuild/bazel-gazelle/config" + "github.com/bazelbuild/bazel-gazelle/rule" "github.com/rs/zerolog" "github.com/stackb/scala-gazelle/pkg/collections" @@ -38,8 +39,8 @@ type Config struct { Logger zerolog.Logger } -// NewConfig returns a pointer to a new Config config with the -// 'Enabled' bit set to true. +// NewConfig returns a pointer to a new config with the 'Enabled' bit set to +// true. func NewConfig(logger zerolog.Logger, config *config.Config, name string) *Config { return &Config{ Logger: logger, @@ -170,3 +171,16 @@ func (c *Config) ParseDirective(d, param, value string) error { return nil } + +// GetRule returns the scalarule.Rule interface that was previously stashed in +// the rule.Rule. +func GetRule(r *rule.Rule) (Rule, bool) { + value := r.PrivateAttr(config.GazelleImportsKey) + impl, ok := value.(Rule) + return impl, ok +} + +// PutRule stashes the scalarule.Rule interface in the rule.Rule. +func PutRule(r *rule.Rule, sr Rule) { + r.SetPrivateAttr(config.GazelleImportsKey, sr) +} diff --git a/pkg/scalarule/package.go b/pkg/scalarule/package.go index 5b7047d7..3bc3d277 100644 --- a/pkg/scalarule/package.go +++ b/pkg/scalarule/package.go @@ -8,9 +8,8 @@ import ( // Package is responsible for instantiating a Rule interface for the given // gazelle.Rule, parsing the attribute name given (typically 'srcs'). type Package interface { - // ParseRule parses the sources from the named attr (typically 'srcs') and - // created a new Rule. - ParseRule(r *grule.Rule, attrName string) (Rule, error) + // NewScalaRule creates new scalarule.Rule from the given rule.Rule. + NewScalaRule(r *grule.Rule) (Rule, error) // GenerateArgs returns the GenerateArgs for the package GenerateArgs() language.GenerateArgs // GeneratedRules returns a list of generated rules in the package. diff --git a/pkg/scalarule/rule.go b/pkg/scalarule/rule.go index 85d92e4b..86557684 100644 --- a/pkg/scalarule/rule.go +++ b/pkg/scalarule/rule.go @@ -10,6 +10,8 @@ import ( // Rule represents a collection of files with their imports and exports. type Rule interface { + // Parse evaluates the source glob and populates the files state + ParseSrcs() error // Exports returns the list of provided symbols that are importable by other // rules. Provides() []resolve.ImportSpec @@ -17,6 +19,6 @@ type Rule interface { Imports(from label.Label) resolver.ImportMap // Import returns the list of required exports for the rule. Exports(from label.Label) resolver.ImportMap - // Files returns the list of files in the Rule - Files() []*sppb.File + // Rule returns the protobuf representation of the rule + Rule() *sppb.Rule } diff --git a/pkg/sweep/BUILD.bazel b/pkg/sweep/BUILD.bazel new file mode 100644 index 00000000..217df48d --- /dev/null +++ b/pkg/sweep/BUILD.bazel @@ -0,0 +1,37 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") +load("@build_stack_scala_gazelle//rules:package_filegroup.bzl", "package_filegroup") + +go_library( + name = "sweep", + srcs = [ + "fix.go", + "sweep.go", + "transitive.go", + ], + importpath = "github.com/stackb/scala-gazelle/pkg/sweep", + visibility = ["//visibility:public"], + deps = [ + "//build/stack/gazelle/scala/parse", + "//pkg/autokeep", + "//pkg/bazel", + "//pkg/collections", + "//pkg/procutil", + "//pkg/resolver", + "//pkg/scalarule", + "@bazel_gazelle//label:go_default_library", + "@bazel_gazelle//rule:go_default_library", + "@com_github_bazelbuild_buildtools//build:go_default_library", + "@com_github_fsnotify_fsnotify//:fsnotify", + "@com_github_pcj_mobyprogress//:mobyprogress", + ], +) + +package_filegroup( + name = "filegroup", + srcs = [ + "BUILD.bazel", + "fix.go", + "sweep.go", + ], + visibility = ["//visibility:public"], +) diff --git a/pkg/sweep/fix.go b/pkg/sweep/fix.go new file mode 100644 index 00000000..f12e51ba --- /dev/null +++ b/pkg/sweep/fix.go @@ -0,0 +1,306 @@ +package sweep + +import ( + "bufio" + "bytes" + "fmt" + "log" + "os/exec" + "path" + "strings" + + "github.com/fsnotify/fsnotify" + "github.com/pcj/mobyprogress" + "github.com/stackb/scala-gazelle/pkg/autokeep" + "github.com/stackb/scala-gazelle/pkg/bazel" + "github.com/stackb/scala-gazelle/pkg/collections" + "github.com/stackb/scala-gazelle/pkg/procutil" + "github.com/stackb/scala-gazelle/pkg/resolver" + "github.com/stackb/scala-gazelle/pkg/scalarule" + + sppb "github.com/stackb/scala-gazelle/build/stack/gazelle/scala/parse" +) + +type ResolvableScalaRuleMap map[scalarule.Rule]func() + +type DepFixer struct { + // progress is the progress interface + progress mobyprogress.Output + // repoRoot is the absolute path to the repository root + repoRoot string + // pkg is the relative path from repoDir + pkg string + // resolvers is a map that contains the resolver function per scala rule + resolvers ResolvableScalaRuleMap + // files is a map that helps identify the rule to which a file belongs + rules map[*sppb.File]scalarule.Rule + // files is a map that helps identify the file by filename + files map[string]*sppb.File + // scope is the (global) scope that should be used to resolve symbols + imports autokeep.DepsMap + // global knownScopes + knownScopes resolver.KnownScopeRegistry + // the global scope + globalScope resolver.Scope +} + +func NewDepFixer(progress mobyprogress.Output, repoRoot, pkg string, resolvers ResolvableScalaRuleMap, imports map[string]string, knownScopes resolver.KnownScopeRegistry, globalScope resolver.Scope) *DepFixer { + fixer := &DepFixer{ + progress: progress, + repoRoot: repoRoot, + pkg: pkg, + resolvers: resolvers, + rules: make(map[*sppb.File]scalarule.Rule), + files: make(map[string]*sppb.File), + imports: imports, + knownScopes: knownScopes, + globalScope: globalScope, + } + for r := range resolvers { + files := r.Rule().Files + for _, f := range files { + fixer.rules[f] = r + fixer.files[f.Filename] = f + } + } + return fixer +} + +func (d *DepFixer) Watch(dir string) error { + // Create new watcher. + watcher, err := fsnotify.NewWatcher() + if err != nil { + log.Fatal(err) + } + defer watcher.Close() + + // Start listening for events. + go func() { + for { + select { + case event, ok := <-watcher.Events: + if !ok { + return + } + log.Println("event:", event) + if event.Has(fsnotify.Write) { + rel := strings.TrimPrefix(strings.TrimPrefix(event.Name, d.repoRoot), "/") + log.Println("modified file:", event.Name, rel) + if err := d.handleChangedFiles(rel); err != nil { + log.Println("watch error:", err) + } + } + case err, ok := <-watcher.Errors: + if !ok { + return + } + log.Println("error:", err) + } + } + }() + + err = watcher.Add(dir) + if err != nil { + log.Fatal(err) + } + + log.Println("watching", dir) + + // Block main goroutine forever. + <-make(chan struct{}) + + return nil +} + +func (d *DepFixer) Batch() error { + changedFiles, err := listChangedScalaFiles(d.repoRoot, d.pkg) + if err != nil { + return err + } + + return d.handleChangedFiles(changedFiles...) +} + +func (d *DepFixer) handleChangedFiles(changedFiles ...string) error { + + log.Println("changed files:", changedFiles) + + // toBuild helps gather the list of rules that need to be built based on the + // list of changed files + toBuild := make(map[string]scalarule.Rule) + + for _, filename := range changedFiles { + if file, ok := d.files[filename]; ok { + rule := d.rules[file] + toBuild[rule.Rule().Label] = rule + } else { + log.Println("no scala build rule known for:", file) + } + } + + targets := make([]string, 0, len(toBuild)) + for label, rule := range toBuild { + targets = append(targets, label) + // re-parse the srcs + if err := rule.ParseSrcs(); err != nil { + return fmt.Errorf("parse %s failed: %v", label, err) + } + // call the resolver for the rule + d.resolvers[rule]() + } + // if err := d.Repair(targets...); err != nil { + // return fmt.Errorf("building rule: %v", err) + // } + + return nil +} + +type RepairHandler interface { + // Targets returns the list of build targets that should be repaired + Targets() []string + // Add is a callback when the given target should add (missing) deps + Add(target string, deps []string) + // Remove is a callback when the given target should remove (unused) deps + Remove(target string, deps []string) + // Apply signals that the current actions should be applied. + Apply(iteration int) error + // Done signals that the repair process has completed + Done() +} + +func (d *DepFixer) Repair(handler RepairHandler) error { + return d.repairIteration(handler, 1) +} + +func (d *DepFixer) repairIteration(handler RepairHandler, iteration int) error { + targets := collections.DeduplicateAndSort(handler.Targets()) + if len(targets) == 0 { + return nil + } + + log.Println("fixing:", targets) + + writeBuildProgress(d.progress, fmt.Sprintf("> bazel build %s", strings.Join(targets, " "))) + + out, exitCode, _ := bazel.ExecCommand("bazel", "build", targets...) + if exitCode == 0 { + log.Println("builds cleanly (no action needed)") + handler.Done() + return nil + } + + diagnostics, err := autokeep.ScanOutput(out) + if err != nil { + return fmt.Errorf("failed to scan build output: %v", err) + } + + delta := autokeep.MakeDeltaDeps(diagnostics, d.imports, d.files, d.knownScopes, d.globalScope) + + toAdd := make(map[string][]string) + toRemove := make(map[string][]string) + + for _, ruleDeps := range delta.Add { + targets = append(targets, ruleDeps.Label) + toAdd[ruleDeps.Label] = append(toAdd[ruleDeps.Label], ruleDeps.Deps...) + } + for _, ruleDeps := range delta.Remove { + targets = append(targets, ruleDeps.Label) + toRemove[ruleDeps.Label] = append(toRemove[ruleDeps.Label], ruleDeps.Deps...) + } + + // if no actions could be derived, the build failed and we don't know what to do. + if len(toAdd) == 0 && len(toRemove) == 0 { + return fmt.Errorf("build failed, but the corrective action(s) could not be determined. Manual intervention is required:\n%s", string(out)) + } + + for ruleLabel, deps := range toAdd { + handler.Add(ruleLabel, collections.SliceDeduplicate(deps)) + } + for ruleLabel, deps := range toRemove { + handler.Remove(ruleLabel, collections.SliceDeduplicate(deps)) + } + + if err := handler.Apply(iteration); err != nil { + return err + } + + return d.repairIteration(handler, iteration+1) +} + +func listChangedScalaFiles(repoDir, pkg string) ([]string, error) { + args := []string{ + "--work-tree", + repoDir, + "ls-files", + "--others", + "--modified", + "--full-name", + "--", + path.Join(pkg, "*.scala"), + } + + cmd := exec.Command("git", args...) + output, err := cmd.CombinedOutput() + exitCode := procutil.CmdExitCode(cmd, err) + + if exitCode != 0 { + return nil, fmt.Errorf("%s failed: %s", args, string(output)) + } + + return scanLsFilesOutput(output) +} + +func scanLsFilesOutput(output []byte) (files []string, err error) { + scanner := bufio.NewScanner(bytes.NewReader(output)) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" { + continue + } + files = append(files, line) + } + if err := scanner.Err(); err != nil { + return nil, err + } + return +} + +func writeBuildProgress(output mobyprogress.Output, message string) { + output.WriteProgress(mobyprogress.Progress{ + ID: "build", + Message: message, + }) +} + +// log.Printf("buildozer 'add deps %s' %s", strings.Join(deps, " "), ruleLabel) +// if err := runBuildozer( +// d.progress, +// fmt.Sprintf("add deps %s", strings.Join(deps, " ")), +// ruleLabel, +// ); err != nil { +// return err +// } +// log.Printf("buildozer 'remove deps %s' %s", strings.Join(deps, " "), ruleLabel) +// if err := runBuildozer( +// d.progress, +// fmt.Sprintf("remove deps %s", strings.Join(deps, " ")), +// ruleLabel, +// ); err != nil { +// return err +// } + +// func runBuildozer(args ...string) error { +// // writeBuildProgress(progress, fmt.Sprintf("> buildozer %s", strings.Join(args, " "))) + +// cmd := exec.Command("buildozer", args...) +// cmd.Dir = bazel.GetBuildWorkspaceDirectory() + +// output, err := cmd.CombinedOutput() +// exitCode := procutil.CmdExitCode(cmd, err) + +// if exitCode != 0 { +// return fmt.Errorf("buildozer failed with exit code %d: %s", exitCode, string(output)) +// } + +// return nil +// } diff --git a/pkg/sweep/sweep.go b/pkg/sweep/sweep.go new file mode 100644 index 00000000..2f9e6ffa --- /dev/null +++ b/pkg/sweep/sweep.go @@ -0,0 +1,243 @@ +package sweep + +import ( + "bytes" + "fmt" + "log" + "os" + "strings" + + "github.com/bazelbuild/bazel-gazelle/label" + "github.com/bazelbuild/bazel-gazelle/rule" + "github.com/bazelbuild/buildtools/build" + "github.com/stackb/scala-gazelle/pkg/bazel" + "github.com/stackb/scala-gazelle/pkg/collections" +) + +const ( + // Turn on the dep sweeper + // + // gazelle:scala_sweep_transitive_deps true + ScalaSweepTransitiveDepsDirective = "scala_sweep_transitive_deps" + + // Turn on the dep repair + // + // gazelle:scala_repair_transitive_deps true + ScalaRepairTransitiveDepsDirective = "scala_repair_transitive_deps" + + // Flag to preserve deps if the label is not known to be needed from the + // imports (legacy migration mode). + // + // gazelle:scala_keep_unknown_deps true + ScalaKeepUnknownDepsDirective = "scala_keep_unknown_deps" + + // Flag to fixup the deps build building the target and parsing scalac output. + // + // gazelle:scala_fix_deps true + ScalaFixDepsDirective = "scala_fix_deps" +) + +const TransitiveCommentToken = "# TRANSITIVE" + +// TransitiveAttr iterates through deps marked "TRANSITIVE" and removes them if the +// target still builds without it. +func TransitiveAttr(attrName string, file *rule.File, r *rule.Rule, from label.Label) (junk []string, err error) { + expr := r.Attr(attrName) + if expr == nil { + return nil, nil + } + + deps, isList := expr.(*build.ListExpr) + if !isList { + return nil, nil // some other condition we can't deal with + } + + // check that the deps have at least one unknown dep + var hasTransitiveDeps bool + for _, expr := range deps.List { + if str, ok := expr.(*build.StringExpr); ok { + for _, suffix := range str.Comment().Suffix { + if suffix.Token == TransitiveCommentToken { + hasTransitiveDeps = true + break + } + } + } + } + if !hasTransitiveDeps { + return nil, nil // nothing to do + } + + // target should build first time, otherwise we can't check accurately. + log.Println("🧱 transitive sweep:", from) + + if out, exitCode, _ := bazel.ExecCommand("bazel", "build", from.String()); exitCode != 0 { + log.Fatalf("sweep failed (must build cleanly on first attempt): %s", string(out)) + } + + // iterate the list backwards + for i := len(deps.List) - 1; i >= 0; i-- { + expr := deps.List[i] + + // look for transitive string dep expressions + dep, ok := expr.(*build.StringExpr) + if !ok { + continue + } + var isTransitiveDep bool + for _, suffix := range dep.Comment().Suffix { + if suffix.Token == TransitiveCommentToken { + isTransitiveDep = true + break + } + } + if !isTransitiveDep { + continue + } + + // reference of original list in case it does not build + original := deps.List + // reset deps with this one spliced out + deps.List = collections.SliceRemoveIndex(deps.List, i) + // save file to reflect change + if err := file.Save(file.Path); err != nil { + return nil, err + } + // see if it still builds + if _, exitCode, _ := bazel.ExecCommand("bazel", "build", from.String()); exitCode == 0 { + log.Println("- 💩 junk:", dep.Value) + junk = append(junk, dep.Value) + AddPostGazelleBuildozerCommand("remove deps "+dep.Value, from.String()) + } else { + log.Println("- 👑 keep:", dep.Value) + deps.List = original + AddPostGazelleBuildozerCommand("commaent deps "+dep.Value+" TRANSITIVE", from.String()) + } + } + + // final save with possible last change + if err := file.Save(file.Path); err != nil { + return nil, err + } + + return +} + +// UnknownAttr iterates through deps in the private attr given and removes them +// if the target still builds without it. +func UnknownAttr(attrName string, file *rule.File, r *rule.Rule, from label.Label) (junk []string, err error) { + unknown, ok := r.PrivateAttr("_unknown_" + attrName).([]string) + if !ok || len(unknown) == 0 { + return nil, nil + } + + // target should build first time, otherwise we can't check accurately. + log.Println("🧱 transitive sweep:", from, len(unknown)) + + if out, exitCode, _ := bazel.ExecCommand("bazel", "build", from.String()); exitCode != 0 { + log.Fatalf("sweep failed (must build cleanly on first attempt): %s", string(out)) + } + + // iterate the deps in the unknown list. Foreach one, remove it and then + // see if the target still builds. If so, put it in the + for _, dep := range unknown { + // remove this dep + if err := runBuildozerCommands(fmt.Sprintf("remove deps %s|%v", dep, from)); err != nil { + return nil, err + } + // see if it still builds + if _, exitCode, _ := bazel.ExecCommand("bazel", "build", from.String()); exitCode == 0 { + log.Println("- 💩 junk:", dep) + junk = append(junk, dep) + AddPostGazelleBuildozerCommand("remove deps "+dep, from.String()) + } else { + log.Println("- 👑 keep:", dep) + AddPostGazelleBuildozerCommand("comment deps "+dep+" TRANSITIVE", from.String()) + } + // restore it + if err := runBuildozerCommands(fmt.Sprintf("add deps %s|%v", dep, from)); err != nil { + return nil, err + } + } + + return +} + +func RemoveSweepDirective(file *rule.File) error { + if file == nil { + return nil + } + // if this file has the sweep directive, remove it + for _, d := range file.Directives { + if d.Key == ScalaSweepTransitiveDepsDirective && d.Value == "true" { + old := []byte(fmt.Sprintf("# gazelle:%s true\n", ScalaSweepTransitiveDepsDirective)) + new := []byte{'\n'} + file.Content = bytes.Replace(file.Content, old, new, -1) + // file.Sync() + if err := file.Save(file.Path); err != nil { + return err + } + // log.Panicln("saved it!", file.Path) + } + } + return nil +} + +func SetKeepDepsDirective(file *rule.File, value bool) error { + if file == nil { + return nil + } + // if this file has the sweep directive, remove it + for _, d := range file.Directives { + if d.Key == ScalaKeepUnknownDepsDirective { + // log.Panicln("found it!", file.Path) + old := []byte(fmt.Sprintf("# gazelle:%s %t", ScalaKeepUnknownDepsDirective, !value)) + new := []byte(fmt.Sprintf("# gazelle:%s %t", ScalaKeepUnknownDepsDirective, value)) + log.Println("OLD:", string(file.Content)) + file.Content = bytes.Replace(file.Content, old, new, -1) + // file.Sync() + log.Println("NEW:", string(file.Content)) + + if err := file.Save(file.Path); err != nil { + return err + } + stat, err := os.Stat(file.Path) + if err != nil { + return err + } + if err := os.WriteFile(file.Path, file.Content, stat.Mode()); err != nil { + return err + } + data, err := os.ReadFile(file.Path) + if err != nil { + return err + } + log.Panicln("saved it!", file.Path, "FILE DATA:\n", string(data)) + } + } + return nil +} + +func MakeTransitiveComment() build.Comment { + return build.Comment{Token: TransitiveCommentToken} +} + +// IsTransitiveDep returns whether e is marked with a "# TRANSITIVE" comment. +func IsTransitiveDep(e build.Expr) bool { + for _, c := range e.Comment().Suffix { + text := strings.TrimSpace(c.Token) + if text == TransitiveCommentToken { + return true + } + } + return false +} + +func HasTransitiveRuleComment(r *rule.Rule) bool { + for _, before := range r.Comments() { + if before == TransitiveCommentToken { + return true + } + } + return false +} diff --git a/pkg/sweep/transitive.go b/pkg/sweep/transitive.go new file mode 100644 index 00000000..c9989e1c --- /dev/null +++ b/pkg/sweep/transitive.go @@ -0,0 +1,203 @@ +package sweep + +import ( + "bytes" + "fmt" + "io" + "log" + "os" + "os/exec" + "sort" + "strings" + + "github.com/bazelbuild/bazel-gazelle/label" + "github.com/bazelbuild/bazel-gazelle/rule" +) + +const ( + REPAIRED = "REPAIRED" + SweepTransitiveDepsTag = "sweep-transitive-deps" +) + +var ( + forRepair = make(map[string]*repairSpec) + postGazelleBuildozerCommands = []string{} +) + +func AddPostGazelleBuildozerCommand(action, target string) { + command := fmt.Sprintf("%s|%s", action, target) + postGazelleBuildozerCommands = append(postGazelleBuildozerCommands, command) +} + +func WritePostGazelleBuildozerFile(filename string) error { + if len(postGazelleBuildozerCommands) == 0 { + return nil + } + content := strings.Join(postGazelleBuildozerCommands, "\n") + return os.WriteFile(filename, []byte(content), os.ModePerm) +} + +type repairSpec struct { + // rule is the rule object to be repaired + rule *rule.Rule + // from is the label as it would appear in a BUILD file + from label.Label + // target is the scala target label that would appear in a log (macro + // expansion), derived from label rewrites + target label.Label +} + +func MarkForTransitiveRepair(rule *rule.Rule, from label.Label, target label.Label) { + spec := &repairSpec{rule, from, target} + forRepair[from.String()] = spec + forRepair[target.String()] = spec +} + +func (d *DepFixer) Transitive() error { + targets := make([]string, 0, len(forRepair)) + for target := range forRepair { + targets = append(targets, target) + } + sort.Strings(targets) + + for _, target := range targets { + spec := forRepair[target] + + if hasRepairedComment(spec.rule) { + log.Printf("%s already repaired (skipping)", target) + } + + handler := NewTransitiveDeltaDepsHandler(spec) + + if err := d.Repair(handler); err != nil { + return fmt.Errorf("building rule: %v", err) + } + } + + return nil +} + +func hasRepairedComment(rule *rule.Rule) bool { + for _, s := range rule.Comments() { + if s == REPAIRED { + return true + } + } + return false +} + +func NewTransitiveDeltaDepsHandler(spec *repairSpec) *TransitiveDeltaDepsHandler { + return &TransitiveDeltaDepsHandler{spec: spec} +} + +type TransitiveDeltaDepsHandler struct { + spec *repairSpec + commands []string +} + +func (h *TransitiveDeltaDepsHandler) queueCommand(action, target string) { + command := fmt.Sprintf("%s|%s", action, target) + log.Println("buildozer:", command) + h.commands = append(h.commands, command) +} + +// Targets implements part of the RepairHandler interface +func (h *TransitiveDeltaDepsHandler) Targets() []string { + return []string{h.spec.from.String()} +} + +// Targets implements part of the RepairHandler interface +func (h *TransitiveDeltaDepsHandler) Add(target string, deps []string) { + if spec, ok := forRepair[target]; ok { + target = spec.from.String() + } + h.queueCommand("add deps "+strings.Join(deps, " "), target) + + for _, dep := range deps { + h.queueCommand("comment deps "+dep+" TRANSITIVE", target) + } +} + +// Remove implements part of the RepairHandler interface +func (h *TransitiveDeltaDepsHandler) Remove(target string, deps []string) { + if spec, ok := forRepair[target]; ok { + target = spec.from.String() + } + h.queueCommand("remove deps "+strings.Join(deps, " "), target) +} + +// Done implements part of the RepairHandler interface +func (h *TransitiveDeltaDepsHandler) Done() { + h.spec.rule.AddComment(REPAIRED) +} + +// TarApplygets implements part of the RepairHandler interface +func (h *TransitiveDeltaDepsHandler) Apply(iteration int) error { + return runBuildozerCommands(h.commands...) +} + +func runBuildozerCommands(commands ...string) error { + // Create the command + cmd := exec.Command("buildozer", "-f", "-") + + // Create a pipe to send input to the command + stdin, err := cmd.StdinPipe() + if err != nil { + return fmt.Errorf("creating stdin pipe: %v", err) + } + + // Create buffers to capture stdout and stderr + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + // Start the command + if err := cmd.Start(); err != nil { + return fmt.Errorf("starting command: %v", err) + } + + // Send input to the command + input := strings.Join(commands, "\n") + log.Println("buildozer input:\n", input) + _, err = io.WriteString(stdin, input) + if err != nil { + return fmt.Errorf("writing to stdin: %v", err) + } + + // Close stdin to signal that we're done sending input + stdin.Close() + + // Wait for the command to finish + if err := cmd.Wait(); err != nil { + return fmt.Errorf("waiting for command (%v): stderr: %s", err, stderr.String()) + } + + // Print the output + return nil +} + +func HasSweepTransitiveDepsTag(r *rule.Rule) bool { + for _, tag := range r.AttrStrings("tags") { + if tag == SweepTransitiveDepsTag { + return true + } + } + return false +} + +func RemoveSweepTransitiveDepsTag(r *rule.Rule) { + tags := make([]string, 0) + + for _, tag := range r.AttrStrings("tags") { + if tag == SweepTransitiveDepsTag { + continue + } + tags = append(tags, tag) + } + + if len(tags) == 0 { + r.DelAttr("tags") + } else { + r.SetAttr("tags", tags) + } +}