forked from bufbuild/buf
/
buflintcheck.go
983 lines (900 loc) · 33.5 KB
/
buflintcheck.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
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
// Copyright 2020-2024 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 buflintcheck impelements the check functions.
//
// These are used by buflintbuild to create RuleBuilders.
package buflintcheck
import (
"errors"
"fmt"
"strconv"
"strings"
"github.com/alis-exchange/buf/private/bufpkg/bufanalysis"
"github.com/alis-exchange/buf/private/bufpkg/bufcheck/buflint/internal/buflintvalidate"
"github.com/alis-exchange/buf/private/bufpkg/bufcheck/internal"
"github.com/alis-exchange/buf/private/pkg/normalpath"
"github.com/alis-exchange/buf/private/pkg/protosource"
"github.com/alis-exchange/buf/private/pkg/protoversion"
"github.com/alis-exchange/buf/private/pkg/slicesext"
"github.com/alis-exchange/buf/private/pkg/stringutil"
)
const (
// CommentIgnorePrefix is the comment ignore prefix.
//
// Comments with this prefix do not count towards valid comments in the comment checkers.
// This is also used in buflint when constructing a new Runner, and is passed to the
// RunnerWithIgnorePrefix option.
CommentIgnorePrefix = "buf:lint:ignore"
)
var (
// CheckCommentEnum is a check function.
CheckCommentEnum = newEnumCheckFunc(checkCommentEnum)
// CheckCommentEnumValue is a check function.
CheckCommentEnumValue = newEnumValueCheckFunc(checkCommentEnumValue)
// CheckCommentField is a check function.
CheckCommentField = newFieldCheckFunc(checkCommentField)
// CheckCommentMessage is a check function.
CheckCommentMessage = newMessageCheckFunc(checkCommentMessage)
// CheckCommentOneof is a check function.
CheckCommentOneof = newOneofCheckFunc(checkCommentOneof)
// CheckCommentService is a check function.
CheckCommentService = newServiceCheckFunc(checkCommentService)
// CheckCommentRPC is a check function.
CheckCommentRPC = newMethodCheckFunc(checkCommentRPC)
)
func checkCommentEnum(add addFunc, value protosource.Enum) error {
return checkCommentNamedDescriptor(add, value, "Enum")
}
func checkCommentEnumValue(add addFunc, value protosource.EnumValue) error {
return checkCommentNamedDescriptor(add, value, "Enum value")
}
func checkCommentField(add addFunc, value protosource.Field) error {
return checkCommentNamedDescriptor(add, value, "Field")
}
func checkCommentMessage(add addFunc, value protosource.Message) error {
return checkCommentNamedDescriptor(add, value, "Message")
}
func checkCommentOneof(add addFunc, value protosource.Oneof) error {
return checkCommentNamedDescriptor(add, value, "Oneof")
}
func checkCommentRPC(add addFunc, value protosource.Method) error {
return checkCommentNamedDescriptor(add, value, "RPC")
}
func checkCommentService(add addFunc, value protosource.Service) error {
return checkCommentNamedDescriptor(add, value, "Service")
}
func checkCommentNamedDescriptor(
add addFunc,
namedDescriptor protosource.NamedDescriptor,
typeName string,
) error {
location := namedDescriptor.Location()
if location == nil {
// this will magically skip map entry fields as well as a side-effect, although originally unintended
return nil
}
if !validLeadingComment(location.LeadingComments()) {
add(namedDescriptor, location, nil, "%s %q should have a non-empty comment for documentation.", typeName, namedDescriptor.Name())
}
return nil
}
// CheckDirectorySamePackage is a check function.
var CheckDirectorySamePackage = newDirToFilesCheckFunc(checkDirectorySamePackage)
func checkDirectorySamePackage(add addFunc, dirPath string, files []protosource.File) error {
pkgMap := make(map[string]struct{})
for _, file := range files {
// works for no package set as this will result in "" which is a valid map key
pkgMap[file.Package()] = struct{}{}
}
if len(pkgMap) > 1 {
var messagePrefix string
if _, ok := pkgMap[""]; ok {
delete(pkgMap, "")
if len(pkgMap) > 1 {
messagePrefix = fmt.Sprintf("Multiple packages %q and file with no package", strings.Join(slicesext.MapKeysToSortedSlice(pkgMap), ","))
} else {
// Join works with only one element as well by adding no comma
messagePrefix = fmt.Sprintf("Package %q and file with no package", strings.Join(slicesext.MapKeysToSortedSlice(pkgMap), ","))
}
} else {
messagePrefix = fmt.Sprintf("Multiple packages %q", strings.Join(slicesext.MapKeysToSortedSlice(pkgMap), ","))
}
for _, file := range files {
add(file, file.PackageLocation(), nil, "%s detected within directory %q.", messagePrefix, dirPath)
}
}
return nil
}
// CheckEnumNoAllowAlias is a check function.
var CheckEnumNoAllowAlias = newEnumCheckFunc(checkEnumNoAllowAlias)
func checkEnumNoAllowAlias(add addFunc, enum protosource.Enum) error {
if enum.AllowAlias() {
add(enum, enum.AllowAliasLocation(), nil, `Enum option "allow_alias" on enum %q must be false.`, enum.Name())
}
return nil
}
// CheckEnumPascalCase is a check function.
var CheckEnumPascalCase = newEnumCheckFunc(checkEnumPascalCase)
func checkEnumPascalCase(add addFunc, enum protosource.Enum) error {
name := enum.Name()
expectedName := stringutil.ToPascalCase(name)
if name != expectedName {
add(enum, enum.NameLocation(), nil, "Enum name %q should be PascalCase, such as %q.", name, expectedName)
}
return nil
}
// CheckEnumFirstValueZero is a check function.
var CheckEnumFirstValueZero = newEnumCheckFunc(checkEnumFirstValueZero)
func checkEnumFirstValueZero(add addFunc, enum protosource.Enum) error {
if values := enum.Values(); len(values) > 0 {
if firstEnumValue := values[0]; firstEnumValue.Number() != 0 {
// proto3 compilation references the number
add(
firstEnumValue,
firstEnumValue.NumberLocation(),
// also check the name location for this comment ignore, as the number location might not have the comment
// see https://github.com/alis-exchange/buf/issues/1186
// also check the enum for this comment ignore
// this allows users to set this "globally" for an enum
// see https://github.com/alis-exchange/buf/issues/161
[]protosource.Location{
firstEnumValue.NameLocation(),
firstEnumValue.Enum().Location(),
},
"First enum value %q should have a numeric value of 0",
firstEnumValue.Name(),
)
}
}
return nil
}
// CheckEnumValuePrefix is a check function.
var CheckEnumValuePrefix = newEnumValueCheckFunc(checkEnumValuePrefix)
func checkEnumValuePrefix(add addFunc, enumValue protosource.EnumValue) error {
name := enumValue.Name()
expectedPrefix := fieldToUpperSnakeCase(enumValue.Enum().Name()) + "_"
if !strings.HasPrefix(name, expectedPrefix) {
add(
enumValue,
enumValue.NameLocation(),
// also check the enum for this comment ignore
// this allows users to set this "globally" for an enum
// this came up in https://github.com/alis-exchange/buf/issues/161
[]protosource.Location{
enumValue.Enum().Location(),
},
"Enum value name %q should be prefixed with %q.",
name,
expectedPrefix,
)
}
return nil
}
// CheckEnumValueUpperSnakeCase is a check function.
var CheckEnumValueUpperSnakeCase = newEnumValueCheckFunc(checkEnumValueUpperSnakeCase)
func checkEnumValueUpperSnakeCase(add addFunc, enumValue protosource.EnumValue) error {
name := enumValue.Name()
expectedName := fieldToUpperSnakeCase(name)
if name != expectedName {
add(
enumValue,
enumValue.NameLocation(),
// also check the enum for this comment ignore
// this allows users to set this "globally" for an enum
[]protosource.Location{
enumValue.Enum().Location(),
},
"Enum value name %q should be UPPER_SNAKE_CASE, such as %q.",
name,
expectedName,
)
}
return nil
}
// CheckEnumZeroValueSuffix is a check function.
var CheckEnumZeroValueSuffix = func(
id string,
ignoreFunc internal.IgnoreFunc,
files []protosource.File,
suffix string,
) ([]bufanalysis.FileAnnotation, error) {
return newEnumValueCheckFunc(
func(add addFunc, enumValue protosource.EnumValue) error {
return checkEnumZeroValueSuffix(add, enumValue, suffix)
},
)(id, ignoreFunc, files)
}
func checkEnumZeroValueSuffix(add addFunc, enumValue protosource.EnumValue, suffix string) error {
if enumValue.Number() != 0 {
return nil
}
name := enumValue.Name()
if !strings.HasSuffix(name, suffix) {
add(
enumValue,
enumValue.NameLocation(),
// also check the enum for this comment ignore
// this allows users to set this "globally" for an enum
[]protosource.Location{
enumValue.Enum().Location(),
},
"Enum zero value name %q should be suffixed with %q.",
name,
suffix,
)
}
return nil
}
// CheckFieldLowerSnakeCase is a check function.
var CheckFieldLowerSnakeCase = newFieldCheckFunc(checkFieldLowerSnakeCase)
func checkFieldLowerSnakeCase(add addFunc, field protosource.Field) error {
message := field.ParentMessage()
if message == nil {
// just a sanity check
return errors.New("field.Message() was nil")
}
if message.IsMapEntry() {
// this check should always pass anyways but just in case
return nil
}
name := field.Name()
expectedName := fieldToLowerSnakeCase(name)
if name != expectedName {
add(
field,
field.NameLocation(),
// also check the message for this comment ignore
// this allows users to set this "globally" for a message
[]protosource.Location{
field.ParentMessage().Location(),
},
"Field name %q should be lower_snake_case, such as %q.",
name,
expectedName,
)
}
return nil
}
// CheckFieldNoDescriptor is a check function.
var CheckFieldNoDescriptor = newFieldCheckFunc(checkFieldNoDescriptor)
func checkFieldNoDescriptor(add addFunc, field protosource.Field) error {
name := field.Name()
if strings.ToLower(strings.Trim(name, "_")) == "descriptor" {
add(
field,
field.NameLocation(),
// also check the message for this comment ignore
// this allows users to set this "globally" for a message
[]protosource.Location{
field.ParentMessage().Location(),
},
`Field name %q cannot be any capitalization of "descriptor" with any number of prefix or suffix underscores.`,
name,
)
}
return nil
}
// CheckFileLowerSnakeCase is a check function.
var CheckFileLowerSnakeCase = newFileCheckFunc(checkFileLowerSnakeCase)
func checkFileLowerSnakeCase(add addFunc, file protosource.File) error {
filename := file.Path()
base := normalpath.Base(filename)
ext := normalpath.Ext(filename)
baseWithoutExt := strings.TrimSuffix(base, ext)
expectedBaseWithoutExt := stringutil.ToLowerSnakeCase(baseWithoutExt)
if baseWithoutExt != expectedBaseWithoutExt {
add(file, nil, nil, `Filename %q should be lower_snake_case%s, such as "%s%s".`, base, ext, expectedBaseWithoutExt, ext)
}
return nil
}
var (
// CheckImportNoPublic is a check function.
CheckImportNoPublic = newFileImportCheckFunc(checkImportNoPublic)
// CheckImportNoWeak is a check function.
CheckImportNoWeak = newFileImportCheckFunc(checkImportNoWeak)
// CheckImportUsed is a check function.
CheckImportUsed = newFileImportCheckFunc(checkImportUsed)
)
func checkImportNoPublic(add addFunc, fileImport protosource.FileImport) error {
return checkImportNoPublicWeak(add, fileImport, fileImport.IsPublic(), "public")
}
func checkImportNoWeak(add addFunc, fileImport protosource.FileImport) error {
return checkImportNoPublicWeak(add, fileImport, fileImport.IsWeak(), "weak")
}
func checkImportNoPublicWeak(add addFunc, fileImport protosource.FileImport, value bool, name string) error {
if value {
add(fileImport, fileImport.Location(), nil, `Import %q must not be %s.`, fileImport.Import(), name)
}
return nil
}
func checkImportUsed(add addFunc, fileImport protosource.FileImport) error {
if fileImport.IsUnused() {
add(fileImport, fileImport.Location(), nil, `Import %q is unused.`, fileImport.Import())
}
return nil
}
// CheckMessagePascalCase is a check function.
var CheckMessagePascalCase = newMessageCheckFunc(checkMessagePascalCase)
func checkMessagePascalCase(add addFunc, message protosource.Message) error {
if message.IsMapEntry() {
// map entries should always be pascal case but we don't want to check them anyways
return nil
}
name := message.Name()
expectedName := stringutil.ToPascalCase(name)
if name != expectedName {
add(message, message.NameLocation(), nil, "Message name %q should be PascalCase, such as %q.", name, expectedName)
}
return nil
}
// CheckOneofLowerSnakeCase is a check function.
var CheckOneofLowerSnakeCase = newOneofCheckFunc(checkOneofLowerSnakeCase)
func checkOneofLowerSnakeCase(add addFunc, oneof protosource.Oneof) error {
name := oneof.Name()
expectedName := fieldToLowerSnakeCase(name)
if name != expectedName {
// if this is an implicit oneof for a proto3 optional field, do not error
// https://github.com/protocolbuffers/protobuf/blob/master/docs/implementing_proto3_presence.md
if fields := oneof.Fields(); len(fields) == 1 {
if fields[0].Proto3Optional() {
return nil
}
}
add(
oneof,
oneof.NameLocation(),
// also check the message for this comment ignore
// this allows users to set this "globally" for a message
[]protosource.Location{
oneof.Message().Location(),
},
"Oneof name %q should be lower_snake_case, such as %q.",
name,
expectedName,
)
}
return nil
}
// CheckPackageDefined is a check function.
var CheckPackageDefined = newFileCheckFunc(checkPackageDefined)
func checkPackageDefined(add addFunc, file protosource.File) error {
if file.Package() == "" {
add(file, nil, nil, "Files must have a package defined.")
}
return nil
}
// CheckPackageDirectoryMatch is a check function.
var CheckPackageDirectoryMatch = newFileCheckFunc(checkPackageDirectoryMatch)
func checkPackageDirectoryMatch(add addFunc, file protosource.File) error {
pkg := file.Package()
if pkg == "" {
return nil
}
expectedDirPath := strings.ReplaceAll(pkg, ".", "/")
dirPath := normalpath.Dir(file.Path())
// need to check case where in root relative directory and no package defined
// this should be valid although if SENSIBLE is turned on this will be invalid
if dirPath != expectedDirPath {
add(file, file.PackageLocation(), nil, `Files with package %q must be within a directory "%s" relative to root but were in directory "%s".`, pkg, normalpath.Unnormalize(expectedDirPath), normalpath.Unnormalize(dirPath))
}
return nil
}
// CheckPackageLowerSnakeCase is a check function.
var CheckPackageLowerSnakeCase = newFileCheckFunc(checkPackageLowerSnakeCase)
func checkPackageLowerSnakeCase(add addFunc, file protosource.File) error {
pkg := file.Package()
if pkg == "" {
return nil
}
split := strings.Split(pkg, ".")
for i, elem := range split {
split[i] = stringutil.ToLowerSnakeCase(elem)
}
expectedPkg := strings.Join(split, ".")
if pkg != expectedPkg {
add(file, file.PackageLocation(), nil, "Package name %q should be lower_snake.case, such as %q.", pkg, expectedPkg)
}
return nil
}
// CheckPackageNoImportCycle is a check function.
//
// Note that imports are not skipped via the helper, as we want to detect import cycles
// even if they are within imports, and report on them. If a non-import is part of an
// import cycle, we report it, even if the import cycle includes imports in it.
var CheckPackageNoImportCycle = newFilesWithImportsCheckFunc(checkPackageNoImportCycle)
func checkPackageNoImportCycle(add addFunc, files []protosource.File) error {
packageToDirectlyImportedPackageToFileImports, err := protosource.PackageToDirectlyImportedPackageToFileImports(files...)
if err != nil {
return err
}
// This is way more algorithmically complex than it needs to be.
//
// We're doing a DFS starting at each package. What we should do is start from any package,
// do the DFS and keep track of the packages hit, and then don't ever do DFS from a given
// package twice. The problem is is that with the current janky package -> direct -> file imports
// setup, we would then end up with error messages like "import cycle: a -> b -> c -> b", and
// attach the error message to a file with package a, and we want to just print "b -> c -> b".
// So to get this to market, we just do a DFS from each package.
//
// This may prove to be too expensive but early testing say it is not so far.
for pkg := range packageToDirectlyImportedPackageToFileImports {
// Can equal "" per the function signature of PackageToDirectlyImportedPackageToFileImports
if pkg == "" {
continue
}
// Go one deep in the potential import cycle so that we can get the file imports
// we want to potentially attach errors to.
//
// We know that pkg is never equal to directlyImportedPackage due to the signature
// of PackageToDirectlyImportedPackageToFileImports.
for directlyImportedPackage, fileImports := range packageToDirectlyImportedPackageToFileImports[pkg] {
// Can equal "" per the function signature of PackageToDirectlyImportedPackageToFileImports
if directlyImportedPackage == "" {
continue
}
if importCycle := getImportCycleIfExists(
directlyImportedPackage,
packageToDirectlyImportedPackageToFileImports,
map[string]struct{}{
pkg: {},
},
[]string{
pkg,
},
); len(importCycle) > 0 {
for _, fileImport := range fileImports {
// We used newFilesWithImportsCheckFunc, meaning that we did not skip imports.
// We do not want to report errors on imports.
if fileImport.File().IsImport() {
continue
}
add(fileImport, fileImport.Location(), nil, `Package import cycle: %s`, strings.Join(importCycle, ` -> `))
}
}
}
}
return nil
}
// CheckPackageSameDirectory is a check function.
var CheckPackageSameDirectory = newPackageToFilesCheckFunc(checkPackageSameDirectory)
func checkPackageSameDirectory(add addFunc, pkg string, files []protosource.File) error {
dirMap := make(map[string]struct{})
for _, file := range files {
dirMap[normalpath.Dir(file.Path())] = struct{}{}
}
if len(dirMap) > 1 {
dirs := slicesext.MapKeysToSortedSlice(dirMap)
for _, file := range files {
add(file, file.PackageLocation(), nil, "Multiple directories %q contain files with package %q.", strings.Join(dirs, ","), pkg)
}
}
return nil
}
var (
// CheckPackageSameCsharpNamespace is a check function.
CheckPackageSameCsharpNamespace = newPackageToFilesCheckFunc(checkPackageSameCsharpNamespace)
// CheckPackageSameGoPackage is a check function.
CheckPackageSameGoPackage = newPackageToFilesCheckFunc(checkPackageSameGoPackage)
// CheckPackageSameJavaMultipleFiles is a check function.
CheckPackageSameJavaMultipleFiles = newPackageToFilesCheckFunc(checkPackageSameJavaMultipleFiles)
// CheckPackageSameJavaPackage is a check function.
CheckPackageSameJavaPackage = newPackageToFilesCheckFunc(checkPackageSameJavaPackage)
// CheckPackageSamePhpNamespace is a check function.
CheckPackageSamePhpNamespace = newPackageToFilesCheckFunc(checkPackageSamePhpNamespace)
// CheckPackageSameRubyPackage is a check function.
CheckPackageSameRubyPackage = newPackageToFilesCheckFunc(checkPackageSameRubyPackage)
// CheckPackageSameSwiftPrefix is a check function.
CheckPackageSameSwiftPrefix = newPackageToFilesCheckFunc(checkPackageSameSwiftPrefix)
)
func checkPackageSameCsharpNamespace(add addFunc, pkg string, files []protosource.File) error {
return checkPackageSameOptionValue(add, pkg, files, protosource.File.CsharpNamespace, protosource.File.CsharpNamespaceLocation, "csharp_namespace")
}
func checkPackageSameGoPackage(add addFunc, pkg string, files []protosource.File) error {
return checkPackageSameOptionValue(add, pkg, files, protosource.File.GoPackage, protosource.File.GoPackageLocation, "go_package")
}
func checkPackageSameJavaMultipleFiles(add addFunc, pkg string, files []protosource.File) error {
return checkPackageSameOptionValue(
add,
pkg,
files,
func(file protosource.File) string {
return strconv.FormatBool(file.JavaMultipleFiles())
},
protosource.File.JavaMultipleFilesLocation,
"java_multiple_files",
)
}
func checkPackageSameJavaPackage(add addFunc, pkg string, files []protosource.File) error {
return checkPackageSameOptionValue(add, pkg, files, protosource.File.JavaPackage, protosource.File.JavaPackageLocation, "java_package")
}
func checkPackageSamePhpNamespace(add addFunc, pkg string, files []protosource.File) error {
return checkPackageSameOptionValue(add, pkg, files, protosource.File.PhpNamespace, protosource.File.PhpNamespaceLocation, "php_namespace")
}
func checkPackageSameRubyPackage(add addFunc, pkg string, files []protosource.File) error {
return checkPackageSameOptionValue(add, pkg, files, protosource.File.RubyPackage, protosource.File.RubyPackageLocation, "ruby_package")
}
func checkPackageSameSwiftPrefix(add addFunc, pkg string, files []protosource.File) error {
return checkPackageSameOptionValue(add, pkg, files, protosource.File.SwiftPrefix, protosource.File.SwiftPrefixLocation, "swift_prefix")
}
func checkPackageSameOptionValue(
add addFunc,
pkg string,
files []protosource.File,
getOptionValue func(protosource.File) string,
getOptionLocation func(protosource.File) protosource.Location,
name string,
) error {
optionValueMap := make(map[string]struct{})
for _, file := range files {
optionValueMap[getOptionValue(file)] = struct{}{}
}
if len(optionValueMap) > 1 {
_, noOptionValue := optionValueMap[""]
delete(optionValueMap, "")
optionValues := slicesext.MapKeysToSortedSlice(optionValueMap)
for _, file := range files {
if noOptionValue {
add(file, getOptionLocation(file), nil, "Files in package %q have both values %q and no value for option %q and all values must be equal.", pkg, strings.Join(optionValues, ","), name)
} else {
add(file, getOptionLocation(file), nil, "Files in package %q have multiple values %q for option %q and all values must be equal.", pkg, strings.Join(optionValues, ","), name)
}
}
}
return nil
}
// CheckPackageVersionSuffix is a check function.
var CheckPackageVersionSuffix = newFileCheckFunc(checkPackageVersionSuffix)
func checkPackageVersionSuffix(add addFunc, file protosource.File) error {
pkg := file.Package()
if pkg == "" {
return nil
}
if _, ok := protoversion.NewPackageVersionForPackage(pkg); !ok {
add(file, file.PackageLocation(), nil, `Package name %q should be suffixed with a correctly formed version, such as %q.`, pkg, pkg+".v1")
}
return nil
}
// CheckProtovalidate is a check function.
var CheckProtovalidate = newFilesWithImportsCheckFunc(checkProtovalidate)
func checkProtovalidate(add addFunc, files []protosource.File) error {
return buflintvalidate.Check(add, files)
}
// CheckRPCNoClientStreaming is a check function.
var CheckRPCNoClientStreaming = newMethodCheckFunc(checkRPCNoClientStreaming)
func checkRPCNoClientStreaming(add addFunc, method protosource.Method) error {
if method.ClientStreaming() {
add(
method,
method.Location(),
// also check the service for this comment ignore
// this allows users to set this "globally" for a service
[]protosource.Location{
method.Service().Location(),
},
"RPC %q is client streaming.",
method.Name(),
)
}
return nil
}
// CheckRPCNoServerStreaming is a check function.
var CheckRPCNoServerStreaming = newMethodCheckFunc(checkRPCNoServerStreaming)
func checkRPCNoServerStreaming(add addFunc, method protosource.Method) error {
if method.ServerStreaming() {
add(
method,
method.Location(),
// also check the service for this comment ignore
// this allows users to set this "globally" for a service
[]protosource.Location{
method.Service().Location(),
},
"RPC %q is server streaming.",
method.Name(),
)
}
return nil
}
// CheckRPCPascalCase is a check function.
var CheckRPCPascalCase = newMethodCheckFunc(checkRPCPascalCase)
func checkRPCPascalCase(add addFunc, method protosource.Method) error {
name := method.Name()
expectedName := stringutil.ToPascalCase(name)
if name != expectedName {
add(
method,
method.NameLocation(),
// also check the service for this comment ignore
// this allows users to set this "globally" for a service
[]protosource.Location{
method.Service().Location(),
},
"RPC name %q should be PascalCase, such as %q.",
name,
expectedName,
)
}
return nil
}
// CheckRPCRequestResponseUnique is a check function.
var CheckRPCRequestResponseUnique = func(
id string,
ignoreFunc internal.IgnoreFunc,
files []protosource.File,
allowSameRequestResponse bool,
allowGoogleProtobufEmptyRequests bool,
allowGoogleProtobufEmptyResponses bool,
) ([]bufanalysis.FileAnnotation, error) {
return newFilesCheckFunc(
func(add addFunc, files []protosource.File) error {
return checkRPCRequestResponseUnique(
add,
files,
allowSameRequestResponse,
allowGoogleProtobufEmptyRequests,
allowGoogleProtobufEmptyResponses,
)
},
)(id, ignoreFunc, files)
}
func checkRPCRequestResponseUnique(
add addFunc,
files []protosource.File,
allowSameRequestResponse bool,
allowGoogleProtobufEmptyRequests bool,
allowGoogleProtobufEmptyResponses bool,
) error {
allFullNameToMethod, err := protosource.FullNameToMethod(files...)
if err != nil {
return err
}
// first check if any requests or responses are the same
// if not, we can treat requests and responses equally for checking if more than
// one method uses a type
if !allowSameRequestResponse {
for _, method := range allFullNameToMethod {
if method.InputTypeName() == method.OutputTypeName() {
// if we allow both empty requests and responses, we do not want to add a FileAnnotation
if !(method.InputTypeName() == "google.protobuf.Empty" && allowGoogleProtobufEmptyRequests && allowGoogleProtobufEmptyResponses) {
add(
method,
method.Location(),
// also check the service for this comment ignore
// this allows users to set this "globally" for a service
[]protosource.Location{
method.Service().Location(),
},
"RPC %q has the same type %q for the request and response.",
method.Name(),
method.InputTypeName(),
)
}
}
}
}
// we have now added errors for the same request and response type if applicable
// we can now check methods for unique usage of a given type
requestResponseTypeToFullNameToMethod := make(map[string]map[string]protosource.Method)
for fullName, method := range allFullNameToMethod {
for _, requestResponseType := range []string{method.InputTypeName(), method.OutputTypeName()} {
fullNameToMethod, ok := requestResponseTypeToFullNameToMethod[requestResponseType]
if !ok {
fullNameToMethod = make(map[string]protosource.Method)
requestResponseTypeToFullNameToMethod[requestResponseType] = fullNameToMethod
}
fullNameToMethod[fullName] = method
}
}
for requestResponseType, fullNameToMethod := range requestResponseTypeToFullNameToMethod {
// only this method uses this request or response type, no issue
if len(fullNameToMethod) == 1 {
continue
}
// if the request or response type is google.protobuf.Empty and we allow this for requests or responses,
// we have to do a harder check
if requestResponseType == "google.protobuf.Empty" && (allowGoogleProtobufEmptyRequests || allowGoogleProtobufEmptyResponses) {
// if both requests and responses can be google.protobuf.Empty, then do not add any error
// else, we check
if !(allowGoogleProtobufEmptyRequests && allowGoogleProtobufEmptyResponses) {
// inside this if statement, one of allowGoogleProtobufEmptyRequests or allowGoogleProtobufEmptyResponses is true
var requestMethods []protosource.Method
var responseMethods []protosource.Method
for _, method := range fullNameToMethod {
if method.InputTypeName() == "google.protobuf.Empty" {
requestMethods = append(requestMethods, method)
}
if method.OutputTypeName() == "google.protobuf.Empty" {
responseMethods = append(responseMethods, method)
}
}
if !allowGoogleProtobufEmptyRequests && len(requestMethods) > 1 {
for _, method := range requestMethods {
add(
method,
method.Location(),
// also check the service for this comment ignore
// this allows users to set this "globally" for a service
[]protosource.Location{
method.Service().Location(),
},
"%q is used as the request for multiple RPCs.",
requestResponseType,
)
}
}
if !allowGoogleProtobufEmptyResponses && len(responseMethods) > 1 {
for _, method := range responseMethods {
add(
method,
method.Location(),
// also check the service for this comment ignore
// this allows users to set this "globally" for a service
[]protosource.Location{
method.Service().Location(),
},
"%q is used as the response for multiple RPCs.",
requestResponseType,
)
}
}
}
} else {
// else, we have a duplicate usage of requestResponseType, add an FileAnnotation to each method
for _, method := range fullNameToMethod {
add(
method,
method.Location(),
// also check the service for this comment ignore
// this allows users to set this "globally" for a service
[]protosource.Location{
method.Service().Location(),
},
"%q is used as the request or response type for multiple RPCs.",
requestResponseType,
)
}
}
}
return nil
}
// CheckRPCRequestStandardName is a check function.
var CheckRPCRequestStandardName = func(
id string,
ignoreFunc internal.IgnoreFunc,
files []protosource.File,
allowGoogleProtobufEmptyRequests bool,
) ([]bufanalysis.FileAnnotation, error) {
return newMethodCheckFunc(
func(add addFunc, method protosource.Method) error {
return checkRPCRequestStandardName(add, method, allowGoogleProtobufEmptyRequests)
},
)(id, ignoreFunc, files)
}
func checkRPCRequestStandardName(add addFunc, method protosource.Method, allowGoogleProtobufEmptyRequests bool) error {
service := method.Service()
if service == nil {
return errors.New("method.Service() is nil")
}
name := method.InputTypeName()
if allowGoogleProtobufEmptyRequests && name == "google.protobuf.Empty" {
return nil
}
if strings.Contains(name, ".") {
split := strings.Split(name, ".")
name = split[len(split)-1]
}
expectedName1 := stringutil.ToPascalCase(method.Name()) + "Request"
expectedName2 := stringutil.ToPascalCase(service.Name()) + expectedName1
if name != expectedName1 && name != expectedName2 {
add(
method,
method.InputTypeLocation(),
// also check the method and service for this comment ignore
// this came up in https://github.com/alis-exchange/buf/issues/242
[]protosource.Location{
method.Location(),
method.Service().Location(),
},
"RPC request type %q should be named %q or %q.",
name,
expectedName1,
expectedName2,
)
}
return nil
}
// CheckRPCResponseStandardName is a check function.
var CheckRPCResponseStandardName = func(
id string,
ignoreFunc internal.IgnoreFunc,
files []protosource.File,
allowGoogleProtobufEmptyResponses bool,
) ([]bufanalysis.FileAnnotation, error) {
return newMethodCheckFunc(
func(add addFunc, method protosource.Method) error {
return checkRPCResponseStandardName(add, method, allowGoogleProtobufEmptyResponses)
},
)(id, ignoreFunc, files)
}
func checkRPCResponseStandardName(add addFunc, method protosource.Method, allowGoogleProtobufEmptyResponses bool) error {
service := method.Service()
if service == nil {
return errors.New("method.Service() is nil")
}
name := method.OutputTypeName()
if allowGoogleProtobufEmptyResponses && name == "google.protobuf.Empty" {
return nil
}
if strings.Contains(name, ".") {
split := strings.Split(name, ".")
name = split[len(split)-1]
}
expectedName1 := stringutil.ToPascalCase(method.Name()) + "Response"
expectedName2 := stringutil.ToPascalCase(service.Name()) + expectedName1
if name != expectedName1 && name != expectedName2 {
add(
method,
method.OutputTypeLocation(),
// also check the method and service for this comment ignore
// this came up in https://github.com/alis-exchange/buf/issues/242
[]protosource.Location{
method.Location(),
method.Service().Location(),
},
"RPC response type %q should be named %q or %q.",
name,
expectedName1,
expectedName2,
)
}
return nil
}
// CheckServicePascalCase is a check function.
var CheckServicePascalCase = newServiceCheckFunc(checkServicePascalCase)
func checkServicePascalCase(add addFunc, service protosource.Service) error {
name := service.Name()
expectedName := stringutil.ToPascalCase(name)
if name != expectedName {
add(service, service.NameLocation(), nil, "Service name %q should be PascalCase, such as %q.", name, expectedName)
}
return nil
}
// CheckServiceSuffix is a check function.
var CheckServiceSuffix = func(
id string,
ignoreFunc internal.IgnoreFunc,
files []protosource.File,
suffix string,
) ([]bufanalysis.FileAnnotation, error) {
return newServiceCheckFunc(
func(add addFunc, service protosource.Service) error {
return checkServiceSuffix(add, service, suffix)
},
)(id, ignoreFunc, files)
}
func checkServiceSuffix(add addFunc, service protosource.Service, suffix string) error {
name := service.Name()
if !strings.HasSuffix(name, suffix) {
add(service, service.NameLocation(), nil, "Service name %q should be suffixed with %q.", name, suffix)
}
return nil
}
// CheckSyntaxSpecified is a check function.
var CheckSyntaxSpecified = newFileCheckFunc(checkSyntaxSpecified)
func checkSyntaxSpecified(add addFunc, file protosource.File) error {
if file.Syntax() == protosource.SyntaxUnspecified {
add(file, file.SyntaxLocation(), nil, `Files must have a syntax explicitly specified. If no syntax is specified, the file defaults to "proto2".`)
}
return nil
}