|
6 | 6 | "google.golang.org/protobuf/types/descriptorpb"
|
7 | 7 | "sort"
|
8 | 8 | "strings"
|
| 9 | + "unicode" |
| 10 | + "unicode/utf8" |
9 | 11 |
|
10 | 12 | "github.com/golang/protobuf/proto"
|
11 | 13 | dpb "github.com/golang/protobuf/protoc-gen-go/descriptor"
|
@@ -75,7 +77,7 @@ func (l *linker) linkFiles() (map[string]*desc.FileDescriptor, error) {
|
75 | 77 | if err := l.checkExtensionsInFile(fd, r); err != nil {
|
76 | 78 | return nil, err
|
77 | 79 | }
|
78 |
| - // and final check for json name configuration |
| 80 | + // and final check for json name conflicts |
79 | 81 | if err := l.checkJsonNamesInFile(fd, r); err != nil {
|
80 | 82 | return nil, err
|
81 | 83 | }
|
@@ -1122,28 +1124,137 @@ func (l *linker) checkJsonNamesInFile(fd *desc.FileDescriptor, res *parseResult)
|
1122 | 1124 | return err
|
1123 | 1125 | }
|
1124 | 1126 | }
|
| 1127 | + for _, ed := range fd.GetEnumTypes() { |
| 1128 | + if err := l.checkJsonNamesInEnum(ed, res); err != nil { |
| 1129 | + return err |
| 1130 | + } |
| 1131 | + } |
1125 | 1132 | return nil
|
1126 | 1133 | }
|
1127 | 1134 |
|
1128 | 1135 | func (l *linker) checkJsonNamesInMessage(md *desc.MessageDescriptor, res *parseResult) error {
|
1129 |
| - if err := checkJsonNames(md, res, false); err != nil { |
| 1136 | + if err := checkFieldJsonNames(md, res, false); err != nil { |
1130 | 1137 | return err
|
1131 | 1138 | }
|
1132 |
| - if err := checkJsonNames(md, res, true); err != nil { |
| 1139 | + if err := checkFieldJsonNames(md, res, true); err != nil { |
1133 | 1140 | return err
|
1134 | 1141 | }
|
| 1142 | + |
| 1143 | + for _, nmd := range md.GetNestedMessageTypes() { |
| 1144 | + if err := l.checkJsonNamesInMessage(nmd, res); err != nil { |
| 1145 | + return err |
| 1146 | + } |
| 1147 | + } |
| 1148 | + for _, ed := range md.GetNestedEnumTypes() { |
| 1149 | + if err := l.checkJsonNamesInEnum(ed, res); err != nil { |
| 1150 | + return err |
| 1151 | + } |
| 1152 | + } |
| 1153 | + return nil |
| 1154 | +} |
| 1155 | + |
| 1156 | +func (l *linker) checkJsonNamesInEnum(ed *desc.EnumDescriptor, res *parseResult) error { |
| 1157 | + seen := map[string]*dpb.EnumValueDescriptorProto{} |
| 1158 | + for _, evd := range ed.GetValues() { |
| 1159 | + scope := "enum value " + ed.GetName() + "." + evd.GetName() |
| 1160 | + |
| 1161 | + name := canonicalEnumValueName(evd.GetName(), ed.GetName()) |
| 1162 | + if existing, ok := seen[name]; ok && evd.GetNumber() != existing.GetNumber() { |
| 1163 | + fldNode := res.getEnumValueNode(evd.AsEnumValueDescriptorProto()) |
| 1164 | + existingNode := res.getEnumValueNode(existing) |
| 1165 | + isProto3 := ed.GetFile().IsProto3() |
| 1166 | + conflictErr := errorWithPos(fldNode.Start(), "%s: camel-case name (with optional enum name prefix removed) %q conflicts with camel-case name of enum value %s, defined at %v", |
| 1167 | + scope, name, existing.GetName(), existingNode.Start()) |
| 1168 | + |
| 1169 | + // Since proto2 did not originally have a JSON format, we report conflicts as just warnings |
| 1170 | + if !isProto3 { |
| 1171 | + res.errs.warn(conflictErr) |
| 1172 | + } else if err := res.errs.handleError(conflictErr); err != nil { |
| 1173 | + return err |
| 1174 | + } |
| 1175 | + } else { |
| 1176 | + seen[name] = evd.AsEnumValueDescriptorProto() |
| 1177 | + } |
| 1178 | + } |
1135 | 1179 | return nil
|
1136 | 1180 | }
|
1137 | 1181 |
|
1138 |
| -func checkJsonNames(md *desc.MessageDescriptor, res *parseResult, useCustom bool) error { |
1139 |
| - type seenName struct { |
| 1182 | +func canonicalEnumValueName(enumValueName, enumName string) string { |
| 1183 | + return enumValCamelCase(removePrefix(enumValueName, enumName)) |
| 1184 | +} |
| 1185 | + |
| 1186 | +// removePrefix is used to remove the given prefix from the given str. It does not require |
| 1187 | +// an exact match and ignores case and underscores. If the all non-underscore characters |
| 1188 | +// would be removed from str, str is returned unchanged. If str does not have the given |
| 1189 | +// prefix (even with the very lenient matching, in regard to case and underscores), then |
| 1190 | +// str is returned unchanged. |
| 1191 | +// |
| 1192 | +// The algorithm is adapted from the protoc source: |
| 1193 | +// https://github.com/protocolbuffers/protobuf/blob/v21.3/src/google/protobuf/descriptor.cc#L922 |
| 1194 | +func removePrefix(str, prefix string) string { |
| 1195 | + j := 0 |
| 1196 | + for i, r := range str { |
| 1197 | + if r == '_' { |
| 1198 | + // skip underscores in the input |
| 1199 | + continue |
| 1200 | + } |
| 1201 | + |
| 1202 | + p, sz := utf8.DecodeRuneInString(prefix[j:]) |
| 1203 | + for p == '_' { |
| 1204 | + j += sz // consume/skip underscore |
| 1205 | + p, sz = utf8.DecodeRuneInString(prefix[j:]) |
| 1206 | + } |
| 1207 | + |
| 1208 | + if j == len(prefix) { |
| 1209 | + // matched entire prefix; return rest of str |
| 1210 | + // but skipping any leading underscores |
| 1211 | + result := strings.TrimLeft(str[i:], "_") |
| 1212 | + if len(result) == 0 { |
| 1213 | + // result can't be empty string |
| 1214 | + return str |
| 1215 | + } |
| 1216 | + return result |
| 1217 | + } |
| 1218 | + if unicode.ToLower(r) != unicode.ToLower(p) { |
| 1219 | + // does not match prefix |
| 1220 | + return str |
| 1221 | + } |
| 1222 | + j += sz // consume matched rune of prefix |
| 1223 | + } |
| 1224 | + return str |
| 1225 | +} |
| 1226 | + |
| 1227 | +// enumValCamelCase converts the given string to upper-camel-case. |
| 1228 | +// |
| 1229 | +// The algorithm is adapted from the protoc source: |
| 1230 | +// https://github.com/protocolbuffers/protobuf/blob/v21.3/src/google/protobuf/descriptor.cc#L887 |
| 1231 | +func enumValCamelCase(name string) string { |
| 1232 | + var js []rune |
| 1233 | + nextUpper := true |
| 1234 | + for _, r := range name { |
| 1235 | + if r == '_' { |
| 1236 | + nextUpper = true |
| 1237 | + continue |
| 1238 | + } |
| 1239 | + if nextUpper { |
| 1240 | + nextUpper = false |
| 1241 | + js = append(js, unicode.ToUpper(r)) |
| 1242 | + } else { |
| 1243 | + js = append(js, unicode.ToLower(r)) |
| 1244 | + } |
| 1245 | + } |
| 1246 | + return string(js) |
| 1247 | +} |
| 1248 | + |
| 1249 | +func checkFieldJsonNames(md *desc.MessageDescriptor, res *parseResult, useCustom bool) error { |
| 1250 | + type jsonName struct { |
1140 | 1251 | source *dpb.FieldDescriptorProto
|
1141 | 1252 | // field's original JSON nane (which can differ in case from map key)
|
1142 | 1253 | orig string
|
1143 | 1254 | // true if orig is a custom JSON name (vs. the field's default JSON name)
|
1144 | 1255 | custom bool
|
1145 | 1256 | }
|
1146 |
| - seen := map[string]seenName{} |
| 1257 | + seen := map[string]jsonName{} |
1147 | 1258 |
|
1148 | 1259 | for _, fd := range md.GetFields() {
|
1149 | 1260 | scope := "field " + md.GetName() + "." + fd.GetName()
|
@@ -1187,7 +1298,7 @@ func checkJsonNames(md *desc.MessageDescriptor, res *parseResult, useCustom bool
|
1187 | 1298 | }
|
1188 | 1299 | }
|
1189 | 1300 | } else {
|
1190 |
| - seen[lcaseName] = seenName{orig: name, source: fd.AsFieldDescriptorProto(), custom: custom} |
| 1301 | + seen[lcaseName] = jsonName{source: fd.AsFieldDescriptorProto(), orig: name, custom: custom} |
1191 | 1302 | }
|
1192 | 1303 | }
|
1193 | 1304 | return nil
|
|
0 commit comments