-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.go
215 lines (204 loc) · 7.76 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
// Copyright 2024-2025 Buf Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package main implements a plugin that implements two Rules:
//
// - A lint Rule that checks that every field has the option (acme.option.v1.safe_for_ml) explicitly set.
// - A breaking Rule that verifes that no field goes from having option (acme.option.v1.safe_for_ml) going
// from true to false. That is, if a field is marked as safe, it can not then be moved to unsafe.
//
// This is an example of a plugin that will check a custom option, which is a very typical
// case for a custom lint or breaking change plugin. In this case, we're saying that an organization
// wants to explicitly mark every field in its schemas as either safe to train ML models on, or
// unsafe to train models on. This plugin enforces that all fields have such markings, and that
// those fields do not transition from safe to unsafe.
//
// This plugin also demonstrates the usage of categories. The Rules have IDs:
//
// - FIELD_OPTION_SAFE_FOR_ML_SET
// - FIELD_OPTION_SAFE_FOR_ML_STAYS_TRUE
//
// However, the Rules both belong to category FIELD_OPTION_SAFE_FOR_ML. This means that you
// do not need to specify the individual rules in your configuration. You can just specify
// the Category, and all Rules in this Category will be included.
//
// To use this plugin:
//
// # buf.yaml
// version: v2
// lint:
// use:
// - STANDARD # omit if you do not want to use the rules builtin to buf
// - FIELD_OPTION_SAFE_FOR_ML
// breaking:
// use:
// - WIRE_JSON # omit if you do not want to use the rules builtin to buf
// - FIELD_OPTION_SAFE_FOR_ML
// plugins:
// - plugin: buf-plugin-field-option-safe-for-ml
package main
import (
"context"
"fmt"
"buf.build/go/bufplugin/check"
"buf.build/go/bufplugin/check/checkutil"
optionv1 "buf.build/go/bufplugin/check/internal/example/gen/acme/option/v1"
"buf.build/go/bufplugin/info"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/descriptorpb"
)
const (
// fieldOptionSafeForMLSetRuleID is the Rule ID of the field option safe for ML set Rule.
fieldOptionSafeForMLSetRuleID = "FIELD_OPTION_SAFE_FOR_ML_SET"
// fieldOptionSafeForMLStaysTrueRuleID is the Rule ID of the field option safe for ML stays true Rule.
fieldOptionSafeForMLStaysTrueRuleID = "FIELD_OPTION_SAFE_FOR_ML_STAYS_TRUE"
// fieldOptionSafeForMLCategoryID is the Category ID for the rules concerning (acme.option.v1.safe_for_ml).
fieldOptionSafeForMLCategoryID = "FIELD_OPTION_SAFE_FOR_ML"
)
var (
// fieldOptionSafeForMLRuleSpec is the RuleSpec for the field option safe for ML set Rule.
fieldOptionSafeForMLSetRuleSpec = &check.RuleSpec{
ID: fieldOptionSafeForMLSetRuleID,
Default: true,
Purpose: "Checks that every field has option (acme.option.v1.safe_for_ml) explicitly set.",
CategoryIDs: []string{
fieldOptionSafeForMLCategoryID,
},
Type: check.RuleTypeLint,
Handler: checkutil.NewFieldRuleHandler(checkFieldOptionSafeForMLSet, checkutil.WithoutImports()),
}
// fieldOptionSafeForMLStaysTrueRuleSpec is the RuleSpec for the field option safe for ML stays true Rule.
fieldOptionSafeForMLStaysTrueRuleSpec = &check.RuleSpec{
ID: fieldOptionSafeForMLStaysTrueRuleID,
Default: true,
Purpose: "Checks that every field marked with (acme.option.v1.safe_for_ml) = true does not change to false.",
CategoryIDs: []string{
fieldOptionSafeForMLCategoryID,
},
Type: check.RuleTypeBreaking,
Handler: checkutil.NewFieldPairRuleHandler(checkFieldOptionSafeForMLStaysTrue, checkutil.WithoutImports()),
}
fieldOptionSafeForMLCategorySpec = &check.CategorySpec{
ID: fieldOptionSafeForMLCategoryID,
Purpose: "Checks properties around the (acme.option.v1.safe_for_ml) option.",
}
// spec is the Spec for the syntax specified plugin.
spec = &check.Spec{
Rules: []*check.RuleSpec{
fieldOptionSafeForMLSetRuleSpec,
fieldOptionSafeForMLStaysTrueRuleSpec,
},
Categories: []*check.CategorySpec{
fieldOptionSafeForMLCategorySpec,
},
// Optional.
Info: &info.Spec{
SPDXLicenseID: "apache-2.0",
LicenseURL: "https://github.com/bufbuild/bufplugin-go/blob/main/LICENSE",
},
}
)
func main() {
check.Main(spec)
}
func checkFieldOptionSafeForMLSet(
_ context.Context,
responseWriter check.ResponseWriter,
_ check.Request,
fieldDescriptor protoreflect.FieldDescriptor,
) error {
// Ignore the actual field options - we don't need to mark safe_for_ml as safe_for_ml.
if fieldDescriptor.ContainingMessage().FullName() == "google.protobuf.FieldOptions" {
return nil
}
fieldOptions, err := getFieldOptions(fieldDescriptor)
if err != nil {
return err
}
if !proto.HasExtension(fieldOptions, optionv1.E_SafeForMl) {
responseWriter.AddAnnotation(
check.WithMessagef(
"Field %q on message %q should have option (acme.option.v1.safe_for_ml) explicitly set.",
fieldDescriptor.Name(),
fieldDescriptor.ContainingMessage().FullName(),
),
check.WithDescriptor(fieldDescriptor),
)
}
return nil
}
func checkFieldOptionSafeForMLStaysTrue(
_ context.Context,
responseWriter check.ResponseWriter,
_ check.Request,
fieldDescriptor protoreflect.FieldDescriptor,
againstFieldDescriptor protoreflect.FieldDescriptor,
) error {
// Ignore the actual field options - we don't need to mark safe_for_ml as safe_for_ml.
if fieldDescriptor.ContainingMessage().FullName() == "google.protobuf.FieldOptions" {
return nil
}
againstSafeForML, err := getSafeForML(againstFieldDescriptor)
if err != nil {
return err
}
if !againstSafeForML {
// If the field does not have safe_for_ml or safe_for_ml is false, we are done. It is up to the
// lint Rule to enforce whether or not every field has this option explicitly set.
return nil
}
safeForML, err := getSafeForML(fieldDescriptor)
if err != nil {
return err
}
if !safeForML {
responseWriter.AddAnnotation(
check.WithMessagef(
"Field %q on message %q should had option (acme.option.v1.safe_for_ml) change from true to false.",
fieldDescriptor.Name(),
fieldDescriptor.ContainingMessage().FullName(),
),
check.WithDescriptor(fieldDescriptor),
check.WithAgainstDescriptor(againstFieldDescriptor),
)
}
return nil
}
func getFieldOptions(fieldDescriptor protoreflect.FieldDescriptor) (*descriptorpb.FieldOptions, error) {
fieldOptions, ok := fieldDescriptor.Options().(*descriptorpb.FieldOptions)
if !ok {
// This should never happen.
return nil, fmt.Errorf("expected *descriptorpb.FieldOptions for FieldDescriptor %q Options but got %T", fieldDescriptor.FullName(), fieldOptions)
}
return fieldOptions, nil
}
func getSafeForML(fieldDescriptor protoreflect.FieldDescriptor) (bool, error) {
fieldOptions, err := getFieldOptions(fieldDescriptor)
if err != nil {
return false, err
}
if !proto.HasExtension(fieldOptions, optionv1.E_SafeForMl) {
return false, nil
}
safeForMLIface := proto.GetExtension(fieldOptions, optionv1.E_SafeForMl)
if safeForMLIface == nil {
return false, fmt.Errorf("expected non-nil value for FieldDescriptor %q option value", fieldDescriptor.FullName())
}
safeForML, ok := safeForMLIface.(bool)
if !ok {
// This should never happen.
return false, fmt.Errorf("expected bool for FieldDescriptor %q option value but got %T", fieldDescriptor.FullName(), safeForMLIface)
}
return safeForML, nil
}