Skip to content

Commit 996e445

Browse files
Explicit handling of proto3 syntax in the parser and static generator
1 parent f2a69a5 commit 996e445

File tree

3 files changed

+42
-13
lines changed

3 files changed

+42
-13
lines changed

cli/targets/static.js

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,27 @@ function toJsType(field) {
354354
return type;
355355
}
356356

357+
function isOptional(type, field) {
358+
359+
// Figure out if a field is explicitly marked optional, depending on the proto syntax (proto2 vs proto3)
360+
// If the syntax has not been recorded in the AST, proto2 semantics will be the default
361+
362+
var syntax = null;
363+
var namespace = type;
364+
365+
while (syntax === null && namespace !== null) {
366+
if (namespace.options != null && "syntax" in namespace.options)
367+
syntax = namespace.options["syntax"]
368+
else
369+
namespace = namespace.parent
370+
}
371+
372+
if (syntax === "proto3")
373+
return field.proto3Optional
374+
else
375+
return field.optional
376+
}
377+
357378
function buildType(ref, type) {
358379

359380
if (config.comments) {
@@ -366,20 +387,25 @@ function buildType(ref, type) {
366387
var prop = util.safeProp(field.name); // either .name or ["name"]
367388
prop = prop.substring(1, prop.charAt(0) === "[" ? prop.length - 1 : prop.length);
368389
var jsType = toJsType(field);
390+
var nullable = false;
369391

370392
// New behaviour - fields explicitly marked as optional and members of a one-of are nullable
371393
// Maps and repeated fields are not explicitly optional, they default to empty instances
372394
if (config["force-optional"]) {
373-
if (field.explicitOptional || field.partOf)
395+
if (isOptional(type, field) || field.partOf) {
374396
jsType = jsType + "|null|undefined";
397+
nullable = true;
398+
}
375399
}
376400
// Old behaviour - field.optional is true for all fields in proto3
377401
else {
378-
if (field.optional)
402+
if (field.optional) {
379403
jsType = jsType + "|null";
404+
nullable = true;
405+
}
380406
}
381407

382-
typeDef.push("@property {" + jsType + "} " + (field.optional ? "[" + prop + "]" : prop) + " " + (field.comment || type.name + " " + field.name));
408+
typeDef.push("@property {" + jsType + "} " + (nullable ? "[" + prop + "]" : prop) + " " + (field.comment || type.name + " " + field.name));
383409
});
384410
push("");
385411
pushComment(typeDef);
@@ -409,7 +435,7 @@ function buildType(ref, type) {
409435
// New behaviour - fields explicitly marked as optional and members of a one-of are nullable
410436
// Maps and repeated fields are not explicitly optional, they default to empty instances
411437
if (config["force-optional"]) {
412-
if (field.explicitOptional || field.partOf)
438+
if (isOptional(type, field) || field.partOf)
413439
jsType = jsType + "|null|undefined";
414440
}
415441
// Old behaviour - field.optional is true for all fields in proto3
@@ -431,7 +457,7 @@ function buildType(ref, type) {
431457
// New behaviour sets a null default when the optional keyword is used explicitly
432458
// Old behaviour considers all proto3 fields optional and uses the null-defaults config flag
433459
var nullDefault = config["force-optional"]
434-
? field.explicitOptional
460+
? isOptional(type, field)
435461
: field.optional && config["null-defaults"];
436462
if (field.repeated)
437463
push(escapeName(type.name) + ".prototype" + prop + " = $util.emptyArray;"); // overwritten in constructor

src/field.js

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,13 @@ function Field(name, id, type, rule, extend, options, comment) {
7878
if (extend !== undefined && !util.isString(extend))
7979
throw TypeError("extend must be a string");
8080

81+
/**
82+
* Explicit record of the proto3 optional rule
83+
* Needed to stop the proto3 optional semantics from getting lost
84+
* @type {boolean}
85+
*/
86+
this.proto3Optional = rule === "proto3_optional";
87+
8188
/**
8289
* Field rule, if any.
8390
* @type {string|undefined}
@@ -117,14 +124,6 @@ function Field(name, id, type, rule, extend, options, comment) {
117124
*/
118125
this.optional = !this.required;
119126

120-
/**
121-
* Whether this field is explicitly marked as optional
122-
* Proto3 fields must use the optional keyword
123-
* Maps and repeated fields are not explicitly optional
124-
* @type {boolean}
125-
*/
126-
this.explicitOptional = rule === "optional";
127-
128127
/**
129128
* Whether this field is repeated.
130129
* @type {boolean}

src/parse.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,10 @@ function parse(source, root, options) {
263263
if (!isProto3 && syntax !== "proto2")
264264
throw illegal(syntax, "syntax");
265265

266+
// Syntax is needed to understand the meaning of the optional field rule
267+
// Otherwise the meaning is ambiguous between proto2 and proto3
268+
root.setOption("syntax", syntax)
269+
266270
skip(";");
267271
}
268272

0 commit comments

Comments
 (0)