@@ -311,9 +311,15 @@ function buildFunction(type, functionName, gen, scope) {
311311 push ( "};" ) ;
312312}
313313
314- function toJsType ( field ) {
314+ function toJsType ( field , parentIsInterface = false ) {
315315 var type ;
316316
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+
317323 switch ( field . type ) {
318324 case "double" :
319325 case "float" :
@@ -341,10 +347,12 @@ function toJsType(field) {
341347 type = "Uint8Array" ;
342348 break ;
343349 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 {
347354 type = "*" ; // should not happen
355+ }
348356 break ;
349357 }
350358 if ( field . map )
@@ -354,8 +362,72 @@ function toJsType(field) {
354362 return type ;
355363}
356364
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+
357427function buildType ( ref , type ) {
358428
429+ var syntax = syntaxForType ( type ) ;
430+
359431 if ( config . comments ) {
360432 var typeDef = [
361433 "Properties of " + aOrAn ( type . name ) + "." ,
@@ -365,10 +437,30 @@ function buildType(ref, type) {
365437 type . fieldsArray . forEach ( function ( field ) {
366438 var prop = util . safeProp ( field . name ) ; // either .name or ["name"]
367439 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 ) ) ;
372464 } ) ;
373465 push ( "" ) ;
374466 pushComment ( typeDef ) ;
@@ -393,9 +485,22 @@ function buildType(ref, type) {
393485 var prop = util . safeProp ( field . name ) ;
394486 if ( config . comments ) {
395487 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+ }
399504 pushComment ( [
400505 field . comment || type . name + " " + field . name + "." ,
401506 "@member {" + jsType + "} " + field . name ,
@@ -406,11 +511,16 @@ function buildType(ref, type) {
406511 push ( "" ) ;
407512 firstField = false ;
408513 }
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" ] ;
409519 if ( field . repeated )
410520 push ( escapeName ( type . name ) + ".prototype" + prop + " = $util.emptyArray;" ) ; // overwritten in constructor
411521 else if ( field . map )
412522 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 )
414524 push ( escapeName ( type . name ) + ".prototype" + prop + " = null;" ) ; // do not set default value for oneof members
415525 else if ( field . long )
416526 push ( escapeName ( type . name ) + ".prototype" + prop + " = $util.Long ? $util.Long.fromBits("
@@ -436,12 +546,17 @@ function buildType(ref, type) {
436546 }
437547 oneof . resolve ( ) ;
438548 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+ }
445560 push ( "Object.defineProperty(" + escapeName ( type . name ) + ".prototype, " + JSON . stringify ( oneof . name ) + ", {" ) ;
446561 ++ indent ;
447562 push ( "get: $util.oneOfGetter($oneOfFields = [" + oneof . oneof . map ( JSON . stringify ) . join ( ", " ) + "])," ) ;
0 commit comments