@@ -311,9 +311,15 @@ function buildFunction(type, functionName, gen, scope) {
311
311
push ( "};" ) ;
312
312
}
313
313
314
- function toJsType ( field ) {
314
+ function toJsType ( field , parentIsInterface = false ) {
315
315
var type ;
316
316
317
+ // With null semantics, interfaces are composed from interfaces and messages from messages
318
+ // Without null semantics, child types depend on the --force-message flag
319
+ var asInterface = config [ "null-semantics" ]
320
+ ? parentIsInterface && ! ( field . resolvedType instanceof protobuf . Enum )
321
+ : ! ( field . resolvedType instanceof protobuf . Enum || config . forceMessage ) ;
322
+
317
323
switch ( field . type ) {
318
324
case "double" :
319
325
case "float" :
@@ -341,10 +347,12 @@ function toJsType(field) {
341
347
type = "Uint8Array" ;
342
348
break ;
343
349
default :
344
- if ( field . resolve ( ) . resolvedType )
345
- type = exportName ( field . resolvedType , ! ( field . resolvedType instanceof protobuf . Enum || config . forceMessage ) ) ;
346
- else
350
+ if ( field . resolve ( ) . resolvedType ) {
351
+ type = exportName ( field . resolvedType , asInterface ) ;
352
+ }
353
+ else {
347
354
type = "*" ; // should not happen
355
+ }
348
356
break ;
349
357
}
350
358
if ( field . map )
@@ -354,8 +362,72 @@ function toJsType(field) {
354
362
return type ;
355
363
}
356
364
365
+ function syntaxForType ( type ) {
366
+
367
+ var syntax = null ;
368
+ var namespace = type ;
369
+
370
+ while ( syntax === null && namespace !== null ) {
371
+ if ( namespace . options != null && "syntax" in namespace . options ) {
372
+ syntax = namespace . options [ "syntax" ] ;
373
+ }
374
+ else {
375
+ namespace = namespace . parent ;
376
+ }
377
+ }
378
+
379
+ return syntax !== null ? syntax : "proto2" ;
380
+ }
381
+
382
+ function isExplicitPresence ( field , syntax ) {
383
+
384
+ // In proto3, optional fields are explicit
385
+ if ( syntax === "proto3" ) {
386
+ return field . options != null && field . options [ "proto3_optional" ] === true ;
387
+ }
388
+
389
+ // In proto2, fields are explicitly optional if they are not part of a map, array or oneOf group
390
+ if ( syntax === "proto2" ) {
391
+ return field . optional && ! ( field . partOf || field . repeated || field . map ) ;
392
+ }
393
+
394
+ throw new Error ( "Unknown proto syntax: [" + syntax + "]" ) ;
395
+ }
396
+
397
+ function isImplicitPresence ( field , syntax ) {
398
+
399
+ // In proto3, everything not marked optional has implicit presence (including maps and repeated fields)
400
+ if ( syntax === "proto3" ) {
401
+ return field . options == null || field . options [ "proto3_optional" ] !== true ;
402
+ }
403
+
404
+ // In proto2, nothing has implicit presence
405
+ if ( syntax === "proto2" ) {
406
+ return false ;
407
+ }
408
+
409
+ throw new Error ( "Unknown proto syntax: [" + syntax + "]" ) ;
410
+ }
411
+
412
+ function isOptionalOneOf ( oneof , syntax ) {
413
+
414
+ if ( syntax === "proto2" ) {
415
+ return false ;
416
+ }
417
+
418
+ if ( oneof . fieldsArray == null || oneof . fieldsArray . length !== 1 ) {
419
+ return false ;
420
+ }
421
+
422
+ var field = oneof . fieldsArray [ 0 ] ;
423
+
424
+ return field . options != null && field . options [ "proto3_optional" ] === true ;
425
+ }
426
+
357
427
function buildType ( ref , type ) {
358
428
429
+ var syntax = syntaxForType ( type ) ;
430
+
359
431
if ( config . comments ) {
360
432
var typeDef = [
361
433
"Properties of " + aOrAn ( type . name ) + "." ,
@@ -365,10 +437,30 @@ function buildType(ref, type) {
365
437
type . fieldsArray . forEach ( function ( field ) {
366
438
var prop = util . safeProp ( field . name ) ; // either .name or ["name"]
367
439
prop = prop . substring ( 1 , prop . charAt ( 0 ) === "[" ? prop . length - 1 : prop . length ) ;
368
- var jsType = toJsType ( field ) ;
369
- if ( field . optional )
370
- jsType = jsType + "|null" ;
371
- typeDef . push ( "@property {" + jsType + "} " + ( field . optional ? "[" + prop + "]" : prop ) + " " + ( field . comment || type . name + " " + field . name ) ) ;
440
+ var jsType = toJsType ( field , /* parentIsInterface = */ true ) ;
441
+ var nullable = false ;
442
+ if ( config [ "null-semantics" ] ) {
443
+ // With semantic nulls, only explicit optional fields and one-of members can be set to null
444
+ // Implicit fields (proto3), maps and lists can be omitted, but if specified must be non-null
445
+ // Implicit fields will take their default value when the message is constructed
446
+ if ( isExplicitPresence ( field , syntax ) || field . partOf ) {
447
+ jsType = jsType + "|null|undefined" ;
448
+ nullable = true ;
449
+ }
450
+ else if ( isImplicitPresence ( field , syntax ) || field . repeated || field . map ) {
451
+ jsType = jsType + "|undefined" ;
452
+ nullable = true ;
453
+ }
454
+ }
455
+ else {
456
+ // Without semantic nulls, everything is optional in proto3
457
+ // Do not allow |undefined to keep backwards compatibility
458
+ if ( field . optional ) {
459
+ jsType = jsType + "|null" ;
460
+ nullable = true ;
461
+ }
462
+ }
463
+ typeDef . push ( "@property {" + jsType + "} " + ( nullable ? "[" + prop + "]" : prop ) + " " + ( field . comment || type . name + " " + field . name ) ) ;
372
464
} ) ;
373
465
push ( "" ) ;
374
466
pushComment ( typeDef ) ;
@@ -393,9 +485,22 @@ function buildType(ref, type) {
393
485
var prop = util . safeProp ( field . name ) ;
394
486
if ( config . comments ) {
395
487
push ( "" ) ;
396
- var jsType = toJsType ( field ) ;
397
- if ( field . optional && ! field . map && ! field . repeated && ( field . resolvedType instanceof Type || config [ "null-defaults" ] ) || field . partOf )
398
- jsType = jsType + "|null|undefined" ;
488
+ var jsType = toJsType ( field , /* parentIsInterface = */ false ) ;
489
+ if ( config [ "null-semantics" ] ) {
490
+ // With semantic nulls, fields are nullable if they are explicitly optional or part of a one-of
491
+ // Maps, repeated values and fields with implicit defaults are never null after construction
492
+ // Members are never undefined, at a minimum they are initialized to null
493
+ if ( isExplicitPresence ( field , syntax ) || field . partOf ) {
494
+ jsType = jsType + "|null" ;
495
+ }
496
+ }
497
+ else {
498
+ // Without semantic nulls, everything is optional in proto3
499
+ // Keep |undefined for backwards compatibility
500
+ if ( field . optional && ! field . map && ! field . repeated && ( field . resolvedType instanceof Type || config [ "null-defaults" ] ) || field . partOf ) {
501
+ jsType = jsType + "|null|undefined" ;
502
+ }
503
+ }
399
504
pushComment ( [
400
505
field . comment || type . name + " " + field . name + "." ,
401
506
"@member {" + jsType + "} " + field . name ,
@@ -406,11 +511,16 @@ function buildType(ref, type) {
406
511
push ( "" ) ;
407
512
firstField = false ;
408
513
}
514
+ // With semantic nulls, only explict optional fields and one-of members are null by default
515
+ // Otherwise use field.optional, which doesn't consider proto3, maps, repeated fields etc.
516
+ var nullDefault = config [ "null-semantics" ]
517
+ ? isExplicitPresence ( field , syntax )
518
+ : field . optional && config [ "null-defaults" ] ;
409
519
if ( field . repeated )
410
520
push ( escapeName ( type . name ) + ".prototype" + prop + " = $util.emptyArray;" ) ; // overwritten in constructor
411
521
else if ( field . map )
412
522
push ( escapeName ( type . name ) + ".prototype" + prop + " = $util.emptyObject;" ) ; // overwritten in constructor
413
- else if ( field . partOf || field . optional && config [ "null-defaults" ] )
523
+ else if ( field . partOf || nullDefault )
414
524
push ( escapeName ( type . name ) + ".prototype" + prop + " = null;" ) ; // do not set default value for oneof members
415
525
else if ( field . long )
416
526
push ( escapeName ( type . name ) + ".prototype" + prop + " = $util.Long ? $util.Long.fromBits("
@@ -436,12 +546,17 @@ function buildType(ref, type) {
436
546
}
437
547
oneof . resolve ( ) ;
438
548
push ( "" ) ;
439
- pushComment ( [
440
- oneof . comment || type . name + " " + oneof . name + "." ,
441
- "@member {" + oneof . oneof . map ( JSON . stringify ) . join ( "|" ) + "|undefined} " + escapeName ( oneof . name ) ,
442
- "@memberof " + exportName ( type ) ,
443
- "@instance"
444
- ] ) ;
549
+ if ( isOptionalOneOf ( oneof , syntax ) ) {
550
+ push ( "// Virtual OneOf for proto3 optional field" ) ;
551
+ }
552
+ else {
553
+ pushComment ( [
554
+ oneof . comment || type . name + " " + oneof . name + "." ,
555
+ "@member {" + oneof . oneof . map ( JSON . stringify ) . join ( "|" ) + "|undefined} " + escapeName ( oneof . name ) ,
556
+ "@memberof " + exportName ( type ) ,
557
+ "@instance"
558
+ ] ) ;
559
+ }
445
560
push ( "Object.defineProperty(" + escapeName ( type . name ) + ".prototype, " + JSON . stringify ( oneof . name ) + ", {" ) ;
446
561
++ indent ;
447
562
push ( "get: $util.oneOfGetter($oneOfFields = [" + oneof . oneof . map ( JSON . stringify ) . join ( ", " ) + "])," ) ;
0 commit comments