Skip to content

Commit 3057e78

Browse files
authored
protoparse: enforce unique JSON names (#523)
- this is (strangely?) a case-insensitive check, just like protoc enforces - this enforces *default* JSON names don't conflict for proto3 files (even for fields that define a custom JSON name) - this also adds checks that custom JSON names don't conflict (unlike protoc) - this will only warn for proto2 files unless the issue is between conflicting *custom* names (JSON mapping was introduced in proto3, so not backwards compatible to fail due to default JSON names in proto2 files)
1 parent 7614117 commit 3057e78

File tree

4 files changed

+281
-4
lines changed

4 files changed

+281
-4
lines changed

desc/protoparse/descriptor_protos.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ func (r *parseResult) createFileDescriptor(filename string, file *ast.FileNode)
3333
fd.Syntax = proto.String(file.Syntax.Syntax.AsString())
3434
}
3535
} else {
36-
r.errs.warn(file.Start(), ErrNoSyntax)
36+
r.errs.warnWithPos(file.Start(), ErrNoSyntax)
3737
}
3838

3939
for _, decl := range file.Decls {

desc/protoparse/errors.go

+8-2
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,18 @@ func (h *errorHandler) handleError(err error) error {
7676
return err
7777
}
7878

79-
func (h *errorHandler) warn(pos *SourcePos, err error) {
79+
func (h *errorHandler) warnWithPos(pos *SourcePos, err error) {
8080
if h.warnReporter != nil {
8181
h.warnReporter(ErrorWithSourcePos{Pos: pos, Underlying: err})
8282
}
8383
}
8484

85+
func (h *errorHandler) warn(err ErrorWithSourcePos) {
86+
if h.warnReporter != nil {
87+
h.warnReporter(err)
88+
}
89+
}
90+
8591
func (h *errorHandler) getError() error {
8692
if h.errsReported > 0 && h.err == nil {
8793
return ErrInvalidSource
@@ -138,7 +144,7 @@ func (e ErrorWithSourcePos) Unwrap() error {
138144

139145
var _ ErrorWithPos = ErrorWithSourcePos{}
140146

141-
func errorWithPos(pos *SourcePos, format string, args ...interface{}) ErrorWithPos {
147+
func errorWithPos(pos *SourcePos, format string, args ...interface{}) ErrorWithSourcePos {
142148
return ErrorWithSourcePos{Pos: pos, Underlying: fmt.Errorf(format, args...)}
143149
}
144150

desc/protoparse/linker.go

+101-1
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ func (l *linker) linkFiles() (map[string]*desc.FileDescriptor, error) {
7575
if err := l.checkExtensionsInFile(fd, r); err != nil {
7676
return nil, err
7777
}
78+
// and final check for json name configuration
79+
if err := l.checkJsonNamesInFile(fd, r); err != nil {
80+
return nil, err
81+
}
7882
}
7983

8084
// When Parser calls linkFiles, it does not check errs again, and it expects that linkFiles
@@ -1051,7 +1055,7 @@ func (l *linker) checkForUnusedImports(filename string) {
10511055
if pos == nil {
10521056
pos = ast.UnknownPos(r.fd.GetName())
10531057
}
1054-
l.errs.warn(pos, errUnusedImport(dep))
1058+
l.errs.warnWithPos(pos, errUnusedImport(dep))
10551059
}
10561060
}
10571061
}
@@ -1111,3 +1115,99 @@ func (l *linker) checkExtension(fld *desc.FieldDescriptor, res *parseResult) err
11111115

11121116
return nil
11131117
}
1118+
1119+
func (l *linker) checkJsonNamesInFile(fd *desc.FileDescriptor, res *parseResult) error {
1120+
for _, md := range fd.GetMessageTypes() {
1121+
if err := l.checkJsonNamesInMessage(md, res); err != nil {
1122+
return err
1123+
}
1124+
}
1125+
return nil
1126+
}
1127+
1128+
func (l *linker) checkJsonNamesInMessage(md *desc.MessageDescriptor, res *parseResult) error {
1129+
if err := checkJsonNames(md, res, false); err != nil {
1130+
return err
1131+
}
1132+
if err := checkJsonNames(md, res, true); err != nil {
1133+
return err
1134+
}
1135+
return nil
1136+
}
1137+
1138+
func checkJsonNames(md *desc.MessageDescriptor, res *parseResult, useCustom bool) error {
1139+
type seenName struct {
1140+
source *dpb.FieldDescriptorProto
1141+
// field's original JSON nane (which can differ in case from map key)
1142+
orig string
1143+
// true if orig is a custom JSON name (vs. the field's default JSON name)
1144+
custom bool
1145+
}
1146+
seen := map[string]seenName{}
1147+
1148+
for _, fd := range md.GetFields() {
1149+
scope := "field " + md.GetName() + "." + fd.GetName()
1150+
defaultName := internal.JsonName(fd.GetName())
1151+
name := defaultName
1152+
custom := false
1153+
if useCustom {
1154+
n := fd.GetJSONName()
1155+
if n != defaultName || hasCustomJsonName(res, fd) {
1156+
name = n
1157+
custom = true
1158+
}
1159+
}
1160+
lcaseName := strings.ToLower(name)
1161+
if existing, ok := seen[lcaseName]; ok {
1162+
// When useCustom is true, we'll only report an issue when a conflict is
1163+
// due to a custom name. That way, we don't double report conflicts on
1164+
// non-custom names.
1165+
if !useCustom || custom || existing.custom {
1166+
fldNode := res.getFieldNode(fd.AsFieldDescriptorProto())
1167+
customStr, srcCustomStr := "custom", "custom"
1168+
if !custom {
1169+
customStr = "default"
1170+
}
1171+
if !existing.custom {
1172+
srcCustomStr = "default"
1173+
}
1174+
otherName := ""
1175+
if name != existing.orig {
1176+
otherName = fmt.Sprintf(" %q", existing.orig)
1177+
}
1178+
conflictErr := errorWithPos(fldNode.Start(), "%s: %s JSON name %q conflicts with %s JSON name%s of field %s, defined at %v",
1179+
scope, customStr, name, srcCustomStr, otherName, existing.source.GetName(), res.getFieldNode(existing.source).Start())
1180+
1181+
// Since proto2 did not originally have default JSON names, we report conflicts involving
1182+
// default names as just warnings.
1183+
if !md.IsProto3() && (!custom || !existing.custom) {
1184+
res.errs.warn(conflictErr)
1185+
} else if err := res.errs.handleError(conflictErr); err != nil {
1186+
return err
1187+
}
1188+
}
1189+
} else {
1190+
seen[lcaseName] = seenName{orig: name, source: fd.AsFieldDescriptorProto(), custom: custom}
1191+
}
1192+
}
1193+
return nil
1194+
}
1195+
1196+
func hasCustomJsonName(res *parseResult, fd *desc.FieldDescriptor) bool {
1197+
// if we have the AST, we can more precisely determine if there was a custom
1198+
// JSON named defined, even if it is explicitly configured to tbe the same
1199+
// as the default JSON name for the field.
1200+
fdProto := fd.AsFieldDescriptorProto()
1201+
opts := res.getFieldNode(fdProto).GetOptions()
1202+
if opts == nil {
1203+
return false
1204+
}
1205+
for _, opt := range opts.Options {
1206+
if len(opt.Name.Parts) == 1 &&
1207+
opt.Name.Parts[0].Name.AsIdentifier() == "json_name" &&
1208+
!opt.Name.Parts[0].IsExtension() {
1209+
return true
1210+
}
1211+
}
1212+
return false
1213+
}

desc/protoparse/linker_test.go

+171
Original file line numberDiff line numberDiff line change
@@ -1006,6 +1006,115 @@ func TestLinkerValidation(t *testing.T) {
10061006
},
10071007
"foo.proto:4:3: field Foo.e: google.protobuf.Struct.FieldsEntry is a synthetic map entry and may not be referenced explicitly",
10081008
},
1009+
{
1010+
map[string]string{
1011+
"foo.proto": "syntax = \"proto3\";\n" +
1012+
"message Foo {\n" +
1013+
" string foo = 1;\n" +
1014+
" string bar = 2 [json_name=\"foo\"];\n" +
1015+
"}\n",
1016+
},
1017+
"foo.proto:4:3: field Foo.bar: custom JSON name \"foo\" conflicts with default JSON name of field foo, defined at foo.proto:3:3",
1018+
},
1019+
{
1020+
map[string]string{
1021+
"foo.proto": "syntax = \"proto3\";\n" +
1022+
"message Foo {\n" +
1023+
" string foo = 1 [json_name=\"foo_bar\"];\n" +
1024+
" string bar = 2 [json_name=\"Foo_Bar\"];\n" +
1025+
"}\n",
1026+
},
1027+
"foo.proto:4:3: field Foo.bar: custom JSON name \"Foo_Bar\" conflicts with custom JSON name \"foo_bar\" of field foo, defined at foo.proto:3:3",
1028+
},
1029+
{
1030+
map[string]string{
1031+
"foo.proto": "syntax = \"proto3\";\n" +
1032+
"message Foo {\n" +
1033+
" string fooBar = 1;\n" +
1034+
" string foo_bar = 2;\n" +
1035+
"}\n",
1036+
},
1037+
"foo.proto:4:3: field Foo.foo_bar: default JSON name \"fooBar\" conflicts with default JSON name of field fooBar, defined at foo.proto:3:3",
1038+
},
1039+
{
1040+
map[string]string{
1041+
"foo.proto": "syntax = \"proto3\";\n" +
1042+
"message Foo {\n" +
1043+
" string fooBar = 1;\n" +
1044+
" string foo_bar = 2 [json_name=\"fuber\"];\n" +
1045+
"}\n",
1046+
},
1047+
"foo.proto:4:3: field Foo.foo_bar: default JSON name \"fooBar\" conflicts with default JSON name of field fooBar, defined at foo.proto:3:3",
1048+
}, {
1049+
map[string]string{
1050+
"foo.proto": "syntax = \"proto3\";\n" +
1051+
"message Foo {\n" +
1052+
" string fooBar = 1;\n" +
1053+
" string FOO_BAR = 2;\n" +
1054+
"}\n",
1055+
},
1056+
"foo.proto:4:3: field Foo.FOO_BAR: default JSON name \"FOOBAR\" conflicts with default JSON name \"fooBar\" of field fooBar, defined at foo.proto:3:3",
1057+
},
1058+
{
1059+
map[string]string{
1060+
"foo.proto": "syntax = \"proto3\";\n" +
1061+
"message Foo {\n" +
1062+
" string fooBar = 1;\n" +
1063+
" string __foo_bar = 2;\n" +
1064+
"}\n",
1065+
},
1066+
"foo.proto:4:3: field Foo.__foo_bar: default JSON name \"FooBar\" conflicts with default JSON name \"fooBar\" of field fooBar, defined at foo.proto:3:3",
1067+
},
1068+
{
1069+
map[string]string{
1070+
"foo.proto": "syntax = \"proto2\";\n" +
1071+
"message Foo {\n" +
1072+
" optional string foo = 1 [json_name=\"foo_bar\"];\n" +
1073+
" optional string bar = 2 [json_name=\"Foo_Bar\"];\n" +
1074+
"}\n",
1075+
},
1076+
"foo.proto:4:3: field Foo.bar: custom JSON name \"Foo_Bar\" conflicts with custom JSON name \"foo_bar\" of field foo, defined at foo.proto:3:3",
1077+
},
1078+
{
1079+
map[string]string{
1080+
"foo.proto": "syntax = \"proto2\";\n" +
1081+
"message Foo {\n" +
1082+
" optional string fooBar = 1;\n" +
1083+
" optional string foo_bar = 2;\n" +
1084+
"}\n",
1085+
},
1086+
"", // should succeed: only check default JSON names in proto3
1087+
},
1088+
{
1089+
map[string]string{
1090+
"foo.proto": "syntax = \"proto2\";\n" +
1091+
"message Foo {\n" +
1092+
" optional string fooBar = 1 [json_name=\"fooBar\"];\n" +
1093+
" optional string foo_bar = 2 [json_name=\"fooBar\"];\n" +
1094+
"}\n",
1095+
},
1096+
"foo.proto:4:3: field Foo.foo_bar: custom JSON name \"fooBar\" conflicts with custom JSON name of field fooBar, defined at foo.proto:3:3",
1097+
},
1098+
{
1099+
map[string]string{
1100+
"foo.proto": "syntax = \"proto2\";\n" +
1101+
"message Foo {\n" +
1102+
" optional string fooBar = 1;\n" +
1103+
" optional string FOO_BAR = 2;\n" +
1104+
"}\n",
1105+
},
1106+
"", // should succeed: only check default JSON names in proto3
1107+
},
1108+
{
1109+
map[string]string{
1110+
"foo.proto": "syntax = \"proto2\";\n" +
1111+
"message Foo {\n" +
1112+
" optional string fooBar = 1;\n" +
1113+
" optional string __foo_bar = 2;\n" +
1114+
"}\n",
1115+
},
1116+
"", // should succeed: only check default JSON names in proto3
1117+
},
10091118
}
10101119
for i, tc := range testCases {
10111120
acc := func(filename string) (io.ReadCloser, error) {
@@ -1180,3 +1289,65 @@ func TestSyntheticOneOfCollisions(t *testing.T) {
11801289
}
11811290
testutil.Eq(t, actual, expected)
11821291
}
1292+
1293+
func TestCustomJSONNameWarnings(t *testing.T) {
1294+
testCases := []struct {
1295+
source string
1296+
warning string
1297+
}{
1298+
{
1299+
source: "syntax = \"proto2\";\n" +
1300+
"message Foo {\n" +
1301+
" optional string foo = 1;\n" +
1302+
" optional string bar = 2 [json_name=\"foo\"];\n" +
1303+
"}\n",
1304+
warning: "test.proto:4:3: field Foo.bar: custom JSON name \"foo\" conflicts with default JSON name of field foo, defined at test.proto:3:3",
1305+
},
1306+
{
1307+
source: "syntax = \"proto2\";\n" +
1308+
"message Foo {\n" +
1309+
" optional string foo_bar = 1;\n" +
1310+
" optional string fooBar = 2;\n" +
1311+
"}\n",
1312+
warning: "test.proto:4:3: field Foo.fooBar: default JSON name \"fooBar\" conflicts with default JSON name of field foo_bar, defined at test.proto:3:3",
1313+
},
1314+
{
1315+
source: "syntax = \"proto2\";\n" +
1316+
"message Foo {\n" +
1317+
" optional string foo_bar = 1;\n" +
1318+
" optional string fooBar = 2;\n" +
1319+
"}\n",
1320+
warning: "test.proto:4:3: field Foo.fooBar: default JSON name \"fooBar\" conflicts with default JSON name of field foo_bar, defined at test.proto:3:3",
1321+
},
1322+
}
1323+
for i, tc := range testCases {
1324+
acc := func(filename string) (io.ReadCloser, error) {
1325+
if filename == "test.proto" {
1326+
return ioutil.NopCloser(strings.NewReader(tc.source)), nil
1327+
}
1328+
return nil, fmt.Errorf("file not found: %s", filename)
1329+
}
1330+
var warnings []string
1331+
warnFunc := func(err ErrorWithPos) {
1332+
warnings = append(warnings, err.Error())
1333+
}
1334+
_, err := Parser{Accessor: acc, WarningReporter: warnFunc}.ParseFiles("test.proto")
1335+
if err != nil {
1336+
t.Errorf("case %d: expecting no error; instead got error %q", i, err)
1337+
}
1338+
if tc.warning == "" && len(warnings) > 0 {
1339+
t.Errorf("case %d: expecting no warnings; instead got: %v", i, warnings)
1340+
} else {
1341+
found := false
1342+
for _, w := range warnings {
1343+
if w == tc.warning {
1344+
found = true
1345+
break
1346+
}
1347+
}
1348+
if !found {
1349+
t.Errorf("case %d: expecting warning %q; instead got: %v", i, tc.warning, warnings)
1350+
}
1351+
}
1352+
}
1353+
}

0 commit comments

Comments
 (0)