protobufjs 8.0.0-experimental → 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/protobuf.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /*!
2
- * protobuf.js v8.0.0-experimental (c) 2016, daniel wirtz
3
- * compiled fri, 21 mar 2025 17:14:14 utc
2
+ * protobuf.js v8.0.0 (c) 2016, daniel wirtz
3
+ * compiled tue, 16 dec 2025 22:00:06 utc
4
4
  * licensed under the bsd-3-clause license
5
5
  * see: https://github.com/dcodeio/protobuf.js for details
6
6
  */
@@ -2108,12 +2108,6 @@ function Enum(name, values, options, comment, comments, valuesOptions) {
2108
2108
  */
2109
2109
  this._valuesFeatures = {};
2110
2110
 
2111
- /**
2112
- * Unresolved values features, if any
2113
- * @type {Object<string, Object<string, *>>|undefined}
2114
- */
2115
- this._valuesProtoFeatures = {};
2116
-
2117
2111
  /**
2118
2112
  * Reserved ranges, if any.
2119
2113
  * @type {Array.<number[]|string>}
@@ -2131,21 +2125,20 @@ function Enum(name, values, options, comment, comments, valuesOptions) {
2131
2125
  }
2132
2126
 
2133
2127
  /**
2134
- * Resolves value features
2135
- * @returns {Enum} `this`
2128
+ * @override
2136
2129
  */
2137
- Enum.prototype.resolve = function resolve() {
2138
- ReflectionObject.prototype.resolve.call(this);
2130
+ Enum.prototype._resolveFeatures = function _resolveFeatures(edition) {
2131
+ edition = this._edition || edition;
2132
+ ReflectionObject.prototype._resolveFeatures.call(this, edition);
2139
2133
 
2140
- for (var key of Object.keys(this._valuesProtoFeatures)) {
2134
+ Object.keys(this.values).forEach(key => {
2141
2135
  var parentFeaturesCopy = Object.assign({}, this._features);
2142
- this._valuesFeatures[key] = Object.assign(parentFeaturesCopy, this._valuesProtoFeatures[key] || {});
2143
- }
2136
+ this._valuesFeatures[key] = Object.assign(parentFeaturesCopy, this.valuesOptions && this.valuesOptions[key] && this.valuesOptions[key].features);
2137
+ });
2144
2138
 
2145
2139
  return this;
2146
2140
  };
2147
2141
 
2148
-
2149
2142
  /**
2150
2143
  * Enum descriptor.
2151
2144
  * @interface IEnum
@@ -2163,6 +2156,9 @@ Enum.prototype.resolve = function resolve() {
2163
2156
  Enum.fromJSON = function fromJSON(name, json) {
2164
2157
  var enm = new Enum(name, json.values, json.options, json.comment, json.comments);
2165
2158
  enm.reserved = json.reserved;
2159
+ if (json.edition)
2160
+ enm._edition = json.edition;
2161
+ enm._defaultEdition = "proto3"; // For backwards-compatibility.
2166
2162
  return enm;
2167
2163
  };
2168
2164
 
@@ -2174,6 +2170,7 @@ Enum.fromJSON = function fromJSON(name, json) {
2174
2170
  Enum.prototype.toJSON = function toJSON(toJSONOptions) {
2175
2171
  var keepComments = toJSONOptions ? Boolean(toJSONOptions.keepComments) : false;
2176
2172
  return util.toObject([
2173
+ "edition" , this._editionToJSON(),
2177
2174
  "options" , this.options,
2178
2175
  "valuesOptions" , this.valuesOptions,
2179
2176
  "values" , this.values,
@@ -2222,21 +2219,6 @@ Enum.prototype.add = function add(name, id, comment, options) {
2222
2219
  if (this.valuesOptions === undefined)
2223
2220
  this.valuesOptions = {};
2224
2221
  this.valuesOptions[name] = options || null;
2225
-
2226
- for (var key of Object.keys(this.valuesOptions)) {
2227
- var features = Array.isArray(this.valuesOptions[key]) ? this.valuesOptions[key].find(x => {return Object.prototype.hasOwnProperty.call(x, "features");}) : this.valuesOptions[key] === "features";
2228
- if (features) {
2229
- this._valuesProtoFeatures[key] = features.features;
2230
- } else {
2231
- this._valuesProtoFeatures[key] = {};
2232
- }
2233
- }
2234
- }
2235
-
2236
- for (var enumValue of Object.keys(this.values)) {
2237
- if (!this._valuesProtoFeatures[enumValue]) {
2238
- this._valuesProtoFeatures[enumValue] = {};
2239
- }
2240
2222
  }
2241
2223
 
2242
2224
  this.comments[name] = comment || null;
@@ -2324,7 +2306,11 @@ var ruleRe = /^required|optional|repeated$/;
2324
2306
  * @throws {TypeError} If arguments are invalid
2325
2307
  */
2326
2308
  Field.fromJSON = function fromJSON(name, json) {
2327
- return new Field(name, json.id, json.type, json.rule, json.extend, json.options, json.comment);
2309
+ var field = new Field(name, json.id, json.type, json.rule, json.extend, json.options, json.comment);
2310
+ if (json.edition)
2311
+ field._edition = json.edition;
2312
+ field._defaultEdition = "proto3"; // For backwards-compatibility.
2313
+ return field;
2328
2314
  };
2329
2315
 
2330
2316
  /**
@@ -2565,6 +2551,7 @@ Field.prototype.setOption = function setOption(name, value, ifNotSet) {
2565
2551
  Field.prototype.toJSON = function toJSON(toJSONOptions) {
2566
2552
  var keepComments = toJSONOptions ? Boolean(toJSONOptions.keepComments) : false;
2567
2553
  return util.toObject([
2554
+ "edition" , this._editionToJSON(),
2568
2555
  "rule" , this.rule !== "optional" && this.rule || undefined,
2569
2556
  "type" , this.type,
2570
2557
  "id" , this.id,
@@ -2649,14 +2636,23 @@ Field.prototype.resolve = function resolve() {
2649
2636
  * @returns {object} The feature values to override
2650
2637
  */
2651
2638
  Field.prototype._inferLegacyProtoFeatures = function _inferLegacyProtoFeatures(edition) {
2652
- if (edition) return {};
2639
+ if (edition !== "proto2" && edition !== "proto3") {
2640
+ return {};
2641
+ }
2653
2642
 
2654
2643
  var features = {};
2644
+
2655
2645
  if (this.rule === "required") {
2656
2646
  features.field_presence = "LEGACY_REQUIRED";
2657
2647
  }
2658
- if (this.resolvedType instanceof Type && this.resolvedType.group) {
2659
- features.message_encoding = "DELIMITED";
2648
+ if (this.parent && types.defaults[this.type] === undefined) {
2649
+ // We can't use resolvedType because types may not have been resolved yet. However,
2650
+ // legacy groups are always in the same scope as the field so we don't have to do a
2651
+ // full scan of the tree.
2652
+ var type = this.parent.get(this.type.split(".").pop());
2653
+ if (type && type instanceof Type && type.group) {
2654
+ features.message_encoding = "DELIMITED";
2655
+ }
2660
2656
  }
2661
2657
  if (this.getOption("packed") === true) {
2662
2658
  features.repeated_field_encoding = "PACKED";
@@ -2666,6 +2662,13 @@ Field.prototype._inferLegacyProtoFeatures = function _inferLegacyProtoFeatures(e
2666
2662
  return features;
2667
2663
  };
2668
2664
 
2665
+ /**
2666
+ * @override
2667
+ */
2668
+ Field.prototype._resolveFeatures = function _resolveFeatures(edition) {
2669
+ return ReflectionObject.prototype._resolveFeatures.call(this, this._edition || edition);
2670
+ };
2671
+
2669
2672
  /**
2670
2673
  * Decorator function as returned by {@link Field.d} and {@link MapField.d} (TypeScript).
2671
2674
  * @typedef FieldDecorator
@@ -3419,10 +3422,40 @@ function Namespace(name, options) {
3419
3422
  * @private
3420
3423
  */
3421
3424
  this._nestedArray = null;
3425
+
3426
+ /**
3427
+ * Cache lookup calls for any objects contains anywhere under this namespace.
3428
+ * This drastically speeds up resolve for large cross-linked protos where the same
3429
+ * types are looked up repeatedly.
3430
+ * @type {Object.<string,ReflectionObject|null>}
3431
+ * @private
3432
+ */
3433
+ this._lookupCache = {};
3434
+
3435
+ /**
3436
+ * Whether or not objects contained in this namespace need feature resolution.
3437
+ * @type {boolean}
3438
+ * @protected
3439
+ */
3440
+ this._needsRecursiveFeatureResolution = true;
3441
+
3442
+ /**
3443
+ * Whether or not objects contained in this namespace need a resolve.
3444
+ * @type {boolean}
3445
+ * @protected
3446
+ */
3447
+ this._needsRecursiveResolve = true;
3422
3448
  }
3423
3449
 
3424
3450
  function clearCache(namespace) {
3425
3451
  namespace._nestedArray = null;
3452
+ namespace._lookupCache = {};
3453
+
3454
+ // Also clear parent caches, since they include nested lookups.
3455
+ var parent = namespace;
3456
+ while(parent = parent.parent) {
3457
+ parent._lookupCache = {};
3458
+ }
3426
3459
  return namespace;
3427
3460
  }
3428
3461
 
@@ -3551,6 +3584,25 @@ Namespace.prototype.add = function add(object) {
3551
3584
  }
3552
3585
  }
3553
3586
  this.nested[object.name] = object;
3587
+
3588
+ if (!(this instanceof Type || this instanceof Service || this instanceof Enum || this instanceof Field)) {
3589
+ // This is a package or a root namespace.
3590
+ if (!object._edition) {
3591
+ // Make sure that some edition is set if it hasn't already been specified.
3592
+ object._edition = object._defaultEdition;
3593
+ }
3594
+ }
3595
+
3596
+ this._needsRecursiveFeatureResolution = true;
3597
+ this._needsRecursiveResolve = true;
3598
+
3599
+ // Also clear parent caches, since they need to recurse down.
3600
+ var parent = this;
3601
+ while(parent = parent.parent) {
3602
+ parent._needsRecursiveFeatureResolution = true;
3603
+ parent._needsRecursiveResolve = true;
3604
+ }
3605
+
3554
3606
  object.onAdd(this);
3555
3607
  return clearCache(this);
3556
3608
  };
@@ -3612,6 +3664,10 @@ Namespace.prototype.define = function define(path, json) {
3612
3664
  * @returns {Namespace} `this`
3613
3665
  */
3614
3666
  Namespace.prototype.resolveAll = function resolveAll() {
3667
+ if (!this._needsRecursiveResolve) return this;
3668
+
3669
+ this._resolveFeaturesRecursive(this._edition);
3670
+
3615
3671
  var nested = this.nestedArray, i = 0;
3616
3672
  this.resolve();
3617
3673
  while (i < nested.length)
@@ -3619,6 +3675,23 @@ Namespace.prototype.resolveAll = function resolveAll() {
3619
3675
  nested[i++].resolveAll();
3620
3676
  else
3621
3677
  nested[i++].resolve();
3678
+ this._needsRecursiveResolve = false;
3679
+ return this;
3680
+ };
3681
+
3682
+ /**
3683
+ * @override
3684
+ */
3685
+ Namespace.prototype._resolveFeaturesRecursive = function _resolveFeaturesRecursive(edition) {
3686
+ if (!this._needsRecursiveFeatureResolution) return this;
3687
+ this._needsRecursiveFeatureResolution = false;
3688
+
3689
+ edition = this._edition || edition;
3690
+
3691
+ ReflectionObject.prototype._resolveFeaturesRecursive.call(this, edition);
3692
+ this.nestedArray.forEach(nested => {
3693
+ nested._resolveFeaturesRecursive(edition);
3694
+ });
3622
3695
  return this;
3623
3696
  };
3624
3697
 
@@ -3630,7 +3703,6 @@ Namespace.prototype.resolveAll = function resolveAll() {
3630
3703
  * @returns {ReflectionObject|null} Looked up object or `null` if none could be found
3631
3704
  */
3632
3705
  Namespace.prototype.lookup = function lookup(path, filterTypes, parentAlreadyChecked) {
3633
-
3634
3706
  /* istanbul ignore next */
3635
3707
  if (typeof filterTypes === "boolean") {
3636
3708
  parentAlreadyChecked = filterTypes;
@@ -3645,29 +3717,72 @@ Namespace.prototype.lookup = function lookup(path, filterTypes, parentAlreadyChe
3645
3717
  } else if (!path.length)
3646
3718
  return this;
3647
3719
 
3720
+ var flatPath = path.join(".");
3721
+
3648
3722
  // Start at root if path is absolute
3649
3723
  if (path[0] === "")
3650
3724
  return this.root.lookup(path.slice(1), filterTypes);
3651
3725
 
3726
+ // Early bailout for objects with matching absolute paths
3727
+ var found = this.root._fullyQualifiedObjects && this.root._fullyQualifiedObjects["." + flatPath];
3728
+ if (found && (!filterTypes || filterTypes.indexOf(found.constructor) > -1)) {
3729
+ return found;
3730
+ }
3731
+
3732
+ // Do a regular lookup at this namespace and below
3733
+ found = this._lookupImpl(path, flatPath);
3734
+ if (found && (!filterTypes || filterTypes.indexOf(found.constructor) > -1)) {
3735
+ return found;
3736
+ }
3737
+
3738
+ if (parentAlreadyChecked)
3739
+ return null;
3740
+
3741
+ // If there hasn't been a match, walk up the tree and look more broadly
3742
+ var current = this;
3743
+ while (current.parent) {
3744
+ found = current.parent._lookupImpl(path, flatPath);
3745
+ if (found && (!filterTypes || filterTypes.indexOf(found.constructor) > -1)) {
3746
+ return found;
3747
+ }
3748
+ current = current.parent;
3749
+ }
3750
+ return null;
3751
+ };
3752
+
3753
+ /**
3754
+ * Internal helper for lookup that handles searching just at this namespace and below along with caching.
3755
+ * @param {string[]} path Path to look up
3756
+ * @param {string} flatPath Flattened version of the path to use as a cache key
3757
+ * @returns {ReflectionObject|null} Looked up object or `null` if none could be found
3758
+ * @private
3759
+ */
3760
+ Namespace.prototype._lookupImpl = function lookup(path, flatPath) {
3761
+ if(Object.prototype.hasOwnProperty.call(this._lookupCache, flatPath)) {
3762
+ return this._lookupCache[flatPath];
3763
+ }
3764
+
3652
3765
  // Test if the first part matches any nested object, and if so, traverse if path contains more
3653
3766
  var found = this.get(path[0]);
3767
+ var exact = null;
3654
3768
  if (found) {
3655
3769
  if (path.length === 1) {
3656
- if (!filterTypes || filterTypes.indexOf(found.constructor) > -1)
3657
- return found;
3658
- } else if (found instanceof Namespace && (found = found.lookup(path.slice(1), filterTypes, true)))
3659
- return found;
3770
+ exact = found;
3771
+ } else if (found instanceof Namespace) {
3772
+ path = path.slice(1);
3773
+ exact = found._lookupImpl(path, path.join("."));
3774
+ }
3660
3775
 
3661
3776
  // Otherwise try each nested namespace
3662
- } else
3777
+ } else {
3663
3778
  for (var i = 0; i < this.nestedArray.length; ++i)
3664
- if (this._nestedArray[i] instanceof Namespace && (found = this._nestedArray[i].lookup(path, filterTypes, true)))
3665
- return found;
3779
+ if (this._nestedArray[i] instanceof Namespace && (found = this._nestedArray[i]._lookupImpl(path, flatPath)))
3780
+ exact = found;
3781
+ }
3666
3782
 
3667
- // If there hasn't been a match, try again at the parent
3668
- if (this.parent === null || parentAlreadyChecked)
3669
- return null;
3670
- return this.parent.lookup(path, filterTypes);
3783
+ // Set this even when null, so that when we walk up the tree we can quickly bail on repeated checks back down.
3784
+ this._lookupCache[flatPath] = exact;
3785
+ return exact;
3671
3786
  };
3672
3787
 
3673
3788
  /**
@@ -3757,9 +3872,10 @@ var Root; // cyclic
3757
3872
 
3758
3873
  /* eslint-disable no-warning-comments */
3759
3874
  // TODO: Replace with embedded proto.
3760
- var editions2023Defaults = {enum_type: "OPEN", field_presence: "EXPLICIT", json_format: "ALLOW", message_encoding: "LENGTH_PREFIXED", repeated_field_encoding: "PACKED", utf8_validation: "VERIFY"};
3761
- var proto2Defaults = {enum_type: "CLOSED", field_presence: "EXPLICIT", json_format: "LEGACY_BEST_EFFORT", message_encoding: "LENGTH_PREFIXED", repeated_field_encoding: "EXPANDED", utf8_validation: "NONE"};
3762
- var proto3Defaults = {enum_type: "OPEN", field_presence: "IMPLICIT", json_format: "ALLOW", message_encoding: "LENGTH_PREFIXED", repeated_field_encoding: "PACKED", utf8_validation: "VERIFY"};
3875
+ var editions2024Defaults = {enum_type: "OPEN", field_presence: "EXPLICIT", json_format: "ALLOW", message_encoding: "LENGTH_PREFIXED", repeated_field_encoding: "PACKED", utf8_validation: "VERIFY", enforce_naming_style: "STYLE2024", default_symbol_visibility: "EXPORT_TOP_LEVEL" };
3876
+ var editions2023Defaults = {enum_type: "OPEN", field_presence: "EXPLICIT", json_format: "ALLOW", message_encoding: "LENGTH_PREFIXED", repeated_field_encoding: "PACKED", utf8_validation: "VERIFY", enforce_naming_style: "STYLE_LEGACY", default_symbol_visibility: "EXPORT_ALL" };
3877
+ var proto2Defaults = {enum_type: "CLOSED", field_presence: "EXPLICIT", json_format: "LEGACY_BEST_EFFORT", message_encoding: "LENGTH_PREFIXED", repeated_field_encoding: "EXPANDED", utf8_validation: "NONE", enforce_naming_style: "STYLE_LEGACY", default_symbol_visibility: "EXPORT_ALL" };
3878
+ var proto3Defaults = {enum_type: "OPEN", field_presence: "IMPLICIT", json_format: "ALLOW", message_encoding: "LENGTH_PREFIXED", repeated_field_encoding: "PACKED", utf8_validation: "VERIFY", enforce_naming_style: "STYLE_LEGACY", default_symbol_visibility: "EXPORT_ALL" };
3763
3879
 
3764
3880
  /**
3765
3881
  * Constructs a new reflection object instance.
@@ -3795,15 +3911,34 @@ function ReflectionObject(name, options) {
3795
3911
  */
3796
3912
  this.name = name;
3797
3913
 
3914
+ /**
3915
+ * The edition specified for this object. Only relevant for top-level objects.
3916
+ * @type {string}
3917
+ * @private
3918
+ */
3919
+ this._edition = null;
3920
+
3921
+ /**
3922
+ * The default edition to use for this object if none is specified. For legacy reasons,
3923
+ * this is proto2 except in the JSON parsing case where it was proto3.
3924
+ * @type {string}
3925
+ * @private
3926
+ */
3927
+ this._defaultEdition = "proto2";
3928
+
3798
3929
  /**
3799
3930
  * Resolved Features.
3931
+ * @type {object}
3932
+ * @private
3800
3933
  */
3801
3934
  this._features = {};
3802
3935
 
3803
3936
  /**
3804
- * Unresolved Features.
3937
+ * Whether or not features have been resolved.
3938
+ * @type {boolean}
3939
+ * @private
3805
3940
  */
3806
- this._protoFeatures = null;
3941
+ this._featuresResolved = false;
3807
3942
 
3808
3943
  /**
3809
3944
  * Parent namespace.
@@ -3910,38 +4045,63 @@ ReflectionObject.prototype.onRemove = function onRemove(parent) {
3910
4045
  ReflectionObject.prototype.resolve = function resolve() {
3911
4046
  if (this.resolved)
3912
4047
  return this;
3913
- if (this instanceof Root || this.parent && this.parent.resolved) {
3914
- this._resolveFeatures();
3915
- this.resolved = true;
3916
- }
4048
+ if (this.root instanceof Root)
4049
+ this.resolved = true; // only if part of a root
3917
4050
  return this;
3918
4051
  };
3919
4052
 
4053
+ /**
4054
+ * Resolves this objects editions features.
4055
+ * @param {string} edition The edition we're currently resolving for.
4056
+ * @returns {ReflectionObject} `this`
4057
+ */
4058
+ ReflectionObject.prototype._resolveFeaturesRecursive = function _resolveFeaturesRecursive(edition) {
4059
+ return this._resolveFeatures(this._edition || edition);
4060
+ };
4061
+
3920
4062
  /**
3921
4063
  * Resolves child features from parent features
4064
+ * @param {string} edition The edition we're currently resolving for.
3922
4065
  * @returns {undefined}
3923
4066
  */
3924
- ReflectionObject.prototype._resolveFeatures = function _resolveFeatures() {
4067
+ ReflectionObject.prototype._resolveFeatures = function _resolveFeatures(edition) {
4068
+ if (this._featuresResolved) {
4069
+ return;
4070
+ }
4071
+
3925
4072
  var defaults = {};
3926
4073
 
3927
- var edition = this.root.getOption("edition");
3928
- if (this instanceof Root) {
3929
- if (this.root.getOption("syntax") === "proto3") {
4074
+ /* istanbul ignore if */
4075
+ if (!edition) {
4076
+ throw new Error("Unknown edition for " + this.fullName);
4077
+ }
4078
+
4079
+ var protoFeatures = Object.assign(this.options ? Object.assign({}, this.options.features) : {},
4080
+ this._inferLegacyProtoFeatures(edition));
4081
+
4082
+ if (this._edition) {
4083
+ // For a namespace marked with a specific edition, reset defaults.
4084
+ /* istanbul ignore else */
4085
+ if (edition === "proto2") {
4086
+ defaults = Object.assign({}, proto2Defaults);
4087
+ } else if (edition === "proto3") {
3930
4088
  defaults = Object.assign({}, proto3Defaults);
3931
4089
  } else if (edition === "2023") {
3932
4090
  defaults = Object.assign({}, editions2023Defaults);
4091
+ } else if (edition === "2024") {
4092
+ defaults = Object.assign({}, editions2024Defaults);
3933
4093
  } else {
3934
- defaults = Object.assign({}, proto2Defaults);
4094
+ throw new Error("Unknown edition: " + edition);
3935
4095
  }
4096
+ this._features = Object.assign(defaults, protoFeatures || {});
4097
+ this._featuresResolved = true;
4098
+ return;
3936
4099
  }
3937
4100
 
3938
- var protoFeatures = Object.assign(Object.assign({}, this._protoFeatures), this._inferLegacyProtoFeatures(edition));
3939
-
3940
- if (this instanceof Root) {
3941
- this._features = Object.assign(defaults, protoFeatures || {});
3942
4101
  // fields in Oneofs aren't actually children of them, so we have to
3943
4102
  // special-case it
3944
- } else if (this.partOf instanceof OneOf) {
4103
+ /* istanbul ignore else */
4104
+ if (this.partOf instanceof OneOf) {
3945
4105
  var lexicalParentFeaturesCopy = Object.assign({}, this.partOf._features);
3946
4106
  this._features = Object.assign(lexicalParentFeaturesCopy, protoFeatures || {});
3947
4107
  } else if (this.declaringField) {
@@ -3950,12 +4110,13 @@ ReflectionObject.prototype._resolveFeatures = function _resolveFeatures() {
3950
4110
  var parentFeaturesCopy = Object.assign({}, this.parent._features);
3951
4111
  this._features = Object.assign(parentFeaturesCopy, protoFeatures || {});
3952
4112
  } else {
3953
- this._features = Object.assign({}, protoFeatures);
4113
+ throw new Error("Unable to find a parent for " + this.fullName);
3954
4114
  }
3955
4115
  if (this.extensionField) {
3956
4116
  // Sister fields should have the same features as their extensions.
3957
4117
  this.extensionField._features = this._features;
3958
4118
  }
4119
+ this._featuresResolved = true;
3959
4120
  };
3960
4121
 
3961
4122
  /**
@@ -3963,7 +4124,6 @@ ReflectionObject.prototype._resolveFeatures = function _resolveFeatures() {
3963
4124
  * in older editions.
3964
4125
  * @param {string|undefined} edition The edition this proto is on, or undefined if pre-editions
3965
4126
  * @returns {object} The feature values to override
3966
- * @abstract
3967
4127
  */
3968
4128
  ReflectionObject.prototype._inferLegacyProtoFeatures = function _inferLegacyProtoFeatures(/*edition*/) {
3969
4129
  return {};
@@ -3984,12 +4144,19 @@ ReflectionObject.prototype.getOption = function getOption(name) {
3984
4144
  * Sets an option.
3985
4145
  * @param {string} name Option name
3986
4146
  * @param {*} value Option value
3987
- * @param {boolean} [ifNotSet] Sets the option only if it isn't currently set
4147
+ * @param {boolean|undefined} [ifNotSet] Sets the option only if it isn't currently set
3988
4148
  * @returns {ReflectionObject} `this`
3989
4149
  */
3990
4150
  ReflectionObject.prototype.setOption = function setOption(name, value, ifNotSet) {
3991
- if (!ifNotSet || !this.options || this.options[name] === undefined)
3992
- (this.options || (this.options = {}))[name] = value;
4151
+ if (!this.options)
4152
+ this.options = {};
4153
+ if (/^features\./.test(name)) {
4154
+ util.setProperty(this.options, name, value, ifNotSet);
4155
+ } else if (!ifNotSet || this.options[name] === undefined) {
4156
+ if (this.getOption(name) !== value) this.resolved = false;
4157
+ this.options[name] = value;
4158
+ }
4159
+
3993
4160
  return this;
3994
4161
  };
3995
4162
 
@@ -4004,7 +4171,6 @@ ReflectionObject.prototype.setParsedOption = function setParsedOption(name, valu
4004
4171
  if (!this.parsedOptions) {
4005
4172
  this.parsedOptions = [];
4006
4173
  }
4007
- var isFeature = /^features$/.test(name);
4008
4174
  var parsedOptions = this.parsedOptions;
4009
4175
  if (propName) {
4010
4176
  // If setting a sub property of an option then try to merge it
@@ -4030,12 +4196,6 @@ ReflectionObject.prototype.setParsedOption = function setParsedOption(name, valu
4030
4196
  parsedOptions.push(newOpt);
4031
4197
  }
4032
4198
 
4033
-
4034
- if (isFeature) {
4035
- var features = parsedOptions.find(x => {return Object.prototype.hasOwnProperty.call(x, "features");});
4036
- this._protoFeatures = features.features || {};
4037
- }
4038
-
4039
4199
  return this;
4040
4200
  };
4041
4201
 
@@ -4064,6 +4224,19 @@ ReflectionObject.prototype.toString = function toString() {
4064
4224
  return className;
4065
4225
  };
4066
4226
 
4227
+ /**
4228
+ * Converts the edition this object is pinned to for JSON format.
4229
+ * @returns {string|undefined} The edition string for JSON representation
4230
+ */
4231
+ ReflectionObject.prototype._editionToJSON = function _editionToJSON() {
4232
+ if (!this._edition || this._edition === "proto3") {
4233
+ // Avoid emitting proto3 since we need to default to it for backwards
4234
+ // compatibility anyway.
4235
+ return undefined;
4236
+ }
4237
+ return this._edition;
4238
+ };
4239
+
4067
4240
  // Sets up cyclic dependencies (called in index-light)
4068
4241
  ReflectionObject._configure = function(Root_) {
4069
4242
  Root = Root_;
@@ -4329,7 +4502,6 @@ var base10Re = /^[1-9][0-9]*$/,
4329
4502
  * @property {string|undefined} package Package name, if declared
4330
4503
  * @property {string[]|undefined} imports Imports, if any
4331
4504
  * @property {string[]|undefined} weakImports Weak imports, if any
4332
- * @property {string|undefined} syntax Syntax, if specified (either `"proto2"` or `"proto3"`)
4333
4505
  * @property {Root} root Populated root instance
4334
4506
  */
4335
4507
 
@@ -4377,14 +4549,25 @@ function parse(source, root, options) {
4377
4549
  pkg,
4378
4550
  imports,
4379
4551
  weakImports,
4380
- syntax,
4381
- edition = false,
4382
- isProto3 = false;
4552
+ edition = "proto2";
4383
4553
 
4384
4554
  var ptr = root;
4385
4555
 
4556
+ var topLevelObjects = [];
4557
+ var topLevelOptions = {};
4558
+
4386
4559
  var applyCase = options.keepCase ? function(name) { return name; } : util.camelCase;
4387
4560
 
4561
+ function resolveFileFeatures() {
4562
+ topLevelObjects.forEach(obj => {
4563
+ obj._edition = edition;
4564
+ Object.keys(topLevelOptions).forEach(opt => {
4565
+ if (obj.getOption(opt) !== undefined) return;
4566
+ obj.setOption(opt, topLevelOptions[opt], true);
4567
+ });
4568
+ });
4569
+ }
4570
+
4388
4571
  /* istanbul ignore next */
4389
4572
  function illegal(token, name, insideTryCatch) {
4390
4573
  var filename = parse.filename;
@@ -4435,13 +4618,17 @@ function parse(source, root, options) {
4435
4618
  function readRanges(target, acceptStrings) {
4436
4619
  var token, start;
4437
4620
  do {
4438
- if (acceptStrings && ((token = peek()) === "\"" || token === "'"))
4439
- target.push(readString());
4440
- else {
4621
+ if (acceptStrings && ((token = peek()) === "\"" || token === "'")) {
4622
+ var str = readString();
4623
+ target.push(str);
4624
+ if (edition >= 2023) {
4625
+ throw illegal(str, "id");
4626
+ }
4627
+ } else {
4441
4628
  try {
4442
4629
  target.push([ start = parseId(next()), skip("to", true) ? parseId(next()) : start ]);
4443
4630
  } catch (err) {
4444
- if (typeRefRe.test(token) && edition) {
4631
+ if (acceptStrings && typeRefRe.test(token) && edition >= 2023) {
4445
4632
  target.push(token);
4446
4633
  } else {
4447
4634
  throw err;
@@ -4524,7 +4711,6 @@ function parse(source, root, options) {
4524
4711
  }
4525
4712
 
4526
4713
  function parsePackage() {
4527
-
4528
4714
  /* istanbul ignore if */
4529
4715
  if (pkg !== undefined)
4530
4716
  throw illegal("package");
@@ -4536,6 +4722,7 @@ function parse(source, root, options) {
4536
4722
  throw illegal(pkg, "name");
4537
4723
 
4538
4724
  ptr = ptr.define(pkg);
4725
+
4539
4726
  skip(";");
4540
4727
  }
4541
4728
 
@@ -4543,6 +4730,16 @@ function parse(source, root, options) {
4543
4730
  var token = peek();
4544
4731
  var whichImports;
4545
4732
  switch (token) {
4733
+ case "option":
4734
+ if (edition < "2024") {
4735
+ throw illegal("option");
4736
+ }
4737
+ // Import options are only used for resolving options, which we don't
4738
+ // do. We can just throw them out.
4739
+ next();
4740
+ readString();
4741
+ skip(";");
4742
+ return;
4546
4743
  case "weak":
4547
4744
  whichImports = weakImports || (weakImports = []);
4548
4745
  next();
@@ -4561,16 +4758,11 @@ function parse(source, root, options) {
4561
4758
 
4562
4759
  function parseSyntax() {
4563
4760
  skip("=");
4564
- syntax = readString();
4565
- isProto3 = syntax === "proto3";
4761
+ edition = readString();
4566
4762
 
4567
4763
  /* istanbul ignore if */
4568
- if (!isProto3 && syntax !== "proto2")
4569
- throw illegal(syntax, "syntax");
4570
-
4571
- // Syntax is needed to understand the meaning of the optional field rule
4572
- // Otherwise the meaning is ambiguous between proto2 and proto3
4573
- root.setOption("syntax", syntax);
4764
+ if (edition < 2023)
4765
+ throw illegal(edition, "syntax");
4574
4766
 
4575
4767
  skip(";");
4576
4768
  }
@@ -4578,14 +4770,12 @@ function parse(source, root, options) {
4578
4770
  function parseEdition() {
4579
4771
  skip("=");
4580
4772
  edition = readString();
4581
- const supportedEditions = ["2023"];
4773
+ const supportedEditions = ["2023", "2024"];
4582
4774
 
4583
4775
  /* istanbul ignore if */
4584
4776
  if (!supportedEditions.includes(edition))
4585
4777
  throw illegal(edition, "edition");
4586
4778
 
4587
- root.setOption("edition", edition);
4588
-
4589
4779
  skip(";");
4590
4780
  }
4591
4781
 
@@ -4606,6 +4796,22 @@ function parse(source, root, options) {
4606
4796
  parseEnum(parent, token);
4607
4797
  return true;
4608
4798
 
4799
+ case "export":
4800
+ case "local":
4801
+ if (edition < "2024") {
4802
+ return false;
4803
+ }
4804
+ token = next();
4805
+ if (token === "export" || token === "local") {
4806
+ return false;
4807
+ }
4808
+ if (token !== "message" && token !== "enum") {
4809
+ return false;
4810
+ }
4811
+ /* eslint-disable no-warning-comments */
4812
+ // TODO: actually enforce visiblity modifiers like protoc does.
4813
+ return parseCommon(parent, token);
4814
+
4609
4815
  case "service":
4610
4816
  parseService(parent, token);
4611
4817
  return true;
@@ -4657,7 +4863,7 @@ function parse(source, root, options) {
4657
4863
  break;
4658
4864
 
4659
4865
  case "required":
4660
- if (edition)
4866
+ if (edition !== "proto2")
4661
4867
  throw illegal(token);
4662
4868
  /* eslint-disable no-fallthrough */
4663
4869
  case "repeated":
@@ -4666,9 +4872,9 @@ function parse(source, root, options) {
4666
4872
 
4667
4873
  case "optional":
4668
4874
  /* istanbul ignore if */
4669
- if (isProto3) {
4875
+ if (edition === "proto3") {
4670
4876
  parseField(type, "proto3_optional");
4671
- } else if (edition) {
4877
+ } else if (edition !== "proto2") {
4672
4878
  throw illegal(token);
4673
4879
  } else {
4674
4880
  parseField(type, "optional");
@@ -4689,7 +4895,7 @@ function parse(source, root, options) {
4689
4895
 
4690
4896
  default:
4691
4897
  /* istanbul ignore if */
4692
- if (!isProto3 && !edition || !typeRefRe.test(token)) {
4898
+ if (edition === "proto2" || !typeRefRe.test(token)) {
4693
4899
  throw illegal(token);
4694
4900
  }
4695
4901
 
@@ -4699,6 +4905,9 @@ function parse(source, root, options) {
4699
4905
  }
4700
4906
  });
4701
4907
  parent.add(type);
4908
+ if (parent === ptr) {
4909
+ topLevelObjects.push(type);
4910
+ }
4702
4911
  }
4703
4912
 
4704
4913
  function parseField(parent, rule, extend) {
@@ -4756,9 +4965,15 @@ function parse(source, root, options) {
4756
4965
  } else {
4757
4966
  parent.add(field);
4758
4967
  }
4968
+ if (parent === ptr) {
4969
+ topLevelObjects.push(field);
4970
+ }
4759
4971
  }
4760
4972
 
4761
4973
  function parseGroup(parent, rule) {
4974
+ if (edition >= 2023) {
4975
+ throw illegal("group");
4976
+ }
4762
4977
  var name = next();
4763
4978
 
4764
4979
  /* istanbul ignore if */
@@ -4788,7 +5003,7 @@ function parse(source, root, options) {
4788
5003
 
4789
5004
  case "optional":
4790
5005
  /* istanbul ignore if */
4791
- if (isProto3) {
5006
+ if (edition === "proto3") {
4792
5007
  parseField(type, "proto3_optional");
4793
5008
  } else {
4794
5009
  parseField(type, "optional");
@@ -4803,6 +5018,28 @@ function parse(source, root, options) {
4803
5018
  parseEnum(type, token);
4804
5019
  break;
4805
5020
 
5021
+ case "reserved":
5022
+ readRanges(type.reserved || (type.reserved = []), true);
5023
+ break;
5024
+
5025
+ case "export":
5026
+ case "local":
5027
+ if (edition < "2024") {
5028
+ throw illegal(token);
5029
+ }
5030
+ token = next();
5031
+ switch (token) {
5032
+ case "message":
5033
+ parseType(type, token);
5034
+ break;
5035
+ case "enum":
5036
+ parseType(type, token);
5037
+ break;
5038
+ default:
5039
+ throw illegal(token);
5040
+ }
5041
+ break;
5042
+
4806
5043
  /* istanbul ignore next */
4807
5044
  default:
4808
5045
  throw illegal(token); // there are no groups with proto3 semantics
@@ -4886,6 +5123,7 @@ function parse(source, root, options) {
4886
5123
 
4887
5124
  case "reserved":
4888
5125
  readRanges(enm.reserved || (enm.reserved = []), true);
5126
+ if(enm.reserved === undefined) enm.reserved = [];
4889
5127
  break;
4890
5128
 
4891
5129
  default:
@@ -4893,6 +5131,9 @@ function parse(source, root, options) {
4893
5131
  }
4894
5132
  });
4895
5133
  parent.add(enm);
5134
+ if (parent === ptr) {
5135
+ topLevelObjects.push(enm);
5136
+ }
4896
5137
  }
4897
5138
 
4898
5139
  function parseEnumValue(parent, token) {
@@ -4906,18 +5147,13 @@ function parse(source, root, options) {
4906
5147
  dummy = {
4907
5148
  options: undefined
4908
5149
  };
5150
+ dummy.getOption = function(name) {
5151
+ return this.options[name];
5152
+ };
4909
5153
  dummy.setOption = function(name, value) {
4910
- if (this.options === undefined)
4911
- this.options = {};
4912
-
4913
- this.options[name] = value;
5154
+ ReflectionObject.prototype.setOption.call(dummy, name, value);
4914
5155
  };
4915
- dummy.setParsedOption = function(name, value, propName) {
4916
- // In order to not change existing behavior, only calling
4917
- // this for features
4918
- if (/^features$/.test(name)) {
4919
- return ReflectionObject.prototype.setParsedOption.call(dummy, name, value, propName);
4920
- }
5156
+ dummy.setParsedOption = function() {
4921
5157
  return undefined;
4922
5158
  };
4923
5159
  ifBlock(dummy, function parseEnumValue_block(token) {
@@ -5034,6 +5270,10 @@ function parse(source, root, options) {
5034
5270
  }
5035
5271
 
5036
5272
  function setOption(parent, name, value) {
5273
+ if (ptr === parent && /^features\./.test(name)) {
5274
+ topLevelOptions[name] = value;
5275
+ return;
5276
+ }
5037
5277
  if (parent.setOption)
5038
5278
  parent.setOption(name, value);
5039
5279
  }
@@ -5072,6 +5312,9 @@ function parse(source, root, options) {
5072
5312
  throw illegal(token);
5073
5313
  });
5074
5314
  parent.add(service);
5315
+ if (parent === ptr) {
5316
+ topLevelObjects.push(service);
5317
+ }
5075
5318
  }
5076
5319
 
5077
5320
  function parseMethod(parent, token) {
@@ -5141,7 +5384,7 @@ function parse(source, root, options) {
5141
5384
 
5142
5385
  case "optional":
5143
5386
  /* istanbul ignore if */
5144
- if (isProto3) {
5387
+ if (edition === "proto3") {
5145
5388
  parseField(parent, "proto3_optional", reference);
5146
5389
  } else {
5147
5390
  parseField(parent, "optional", reference);
@@ -5150,7 +5393,7 @@ function parse(source, root, options) {
5150
5393
 
5151
5394
  default:
5152
5395
  /* istanbul ignore if */
5153
- if (!isProto3 && !edition || !typeRefRe.test(token))
5396
+ if (edition === "proto2" || !typeRefRe.test(token))
5154
5397
  throw illegal(token);
5155
5398
  push(token);
5156
5399
  parseField(parent, "optional", reference);
@@ -5215,12 +5458,13 @@ function parse(source, root, options) {
5215
5458
  }
5216
5459
  }
5217
5460
 
5461
+ resolveFileFeatures();
5462
+
5218
5463
  parse.filename = null;
5219
5464
  return {
5220
5465
  "package" : pkg,
5221
5466
  "imports" : imports,
5222
5467
  weakImports : weakImports,
5223
- syntax : syntax,
5224
5468
  root : root
5225
5469
  };
5226
5470
  }
@@ -5746,6 +5990,20 @@ function Root(options) {
5746
5990
  * @type {string[]}
5747
5991
  */
5748
5992
  this.files = [];
5993
+
5994
+ /**
5995
+ * Edition, defaults to proto2 if unspecified.
5996
+ * @type {string}
5997
+ * @private
5998
+ */
5999
+ this._edition = "proto2";
6000
+
6001
+ /**
6002
+ * Global lookup cache of fully qualified names.
6003
+ * @type {Object.<string,ReflectionObject>}
6004
+ * @private
6005
+ */
6006
+ this._fullyQualifiedObjects = {};
5749
6007
  }
5750
6008
 
5751
6009
  /**
@@ -5759,7 +6017,7 @@ Root.fromJSON = function fromJSON(json, root) {
5759
6017
  root = new Root();
5760
6018
  if (json.options)
5761
6019
  root.setOptions(json.options);
5762
- return root.addJSON(json.nested);
6020
+ return root.addJSON(json.nested).resolveAll();
5763
6021
  };
5764
6022
 
5765
6023
  /**
@@ -5814,11 +6072,11 @@ Root.prototype.load = function load(filename, options, callback) {
5814
6072
  if (sync) {
5815
6073
  throw err;
5816
6074
  }
5817
- var cb = callback;
5818
- callback = null;
5819
6075
  if (root) {
5820
6076
  root.resolveAll();
5821
6077
  }
6078
+ var cb = callback;
6079
+ callback = null;
5822
6080
  cb(err, root);
5823
6081
  }
5824
6082
 
@@ -5926,8 +6184,8 @@ Root.prototype.load = function load(filename, options, callback) {
5926
6184
  for (var i = 0, resolved; i < filename.length; ++i)
5927
6185
  if (resolved = self.resolvePath("", filename[i]))
5928
6186
  fetch(resolved);
5929
- self.resolveAll();
5930
6187
  if (sync) {
6188
+ self.resolveAll();
5931
6189
  return self;
5932
6190
  }
5933
6191
  if (!queued) {
@@ -5976,6 +6234,8 @@ Root.prototype.loadSync = function loadSync(filename, options) {
5976
6234
  * @override
5977
6235
  */
5978
6236
  Root.prototype.resolveAll = function resolveAll() {
6237
+ if (!this._needsRecursiveResolve) return this;
6238
+
5979
6239
  if (this.deferred.length)
5980
6240
  throw Error("unresolvable extensions: " + this.deferred.map(function(field) {
5981
6241
  return "'extend " + field.extend + "' in " + field.parent.fullName;
@@ -6042,6 +6302,11 @@ Root.prototype._handleAdd = function _handleAdd(object) {
6042
6302
  object.parent[object.name] = object; // expose namespace as property of its parent
6043
6303
  }
6044
6304
 
6305
+ if (object instanceof Type || object instanceof Enum || object instanceof Field) {
6306
+ // Only store types and enums for quick lookup during resolve.
6307
+ this._fullyQualifiedObjects[object.fullName] = object;
6308
+ }
6309
+
6045
6310
  // The above also adds uppercased (and thus conflict-free) nested types, services and enums as
6046
6311
  // properties of namespaces just like static code does. This allows using a .d.ts generated for
6047
6312
  // a static module with reflection-based solutions where the condition is met.
@@ -6082,6 +6347,8 @@ Root.prototype._handleRemove = function _handleRemove(object) {
6082
6347
  delete object.parent[object.name]; // unexpose namespaces
6083
6348
 
6084
6349
  }
6350
+
6351
+ delete this._fullyQualifiedObjects[object.fullName];
6085
6352
  };
6086
6353
 
6087
6354
  // Sets up cyclic dependencies (called in index-light)
@@ -6353,7 +6620,10 @@ Service.fromJSON = function fromJSON(name, json) {
6353
6620
  service.add(Method.fromJSON(names[i], json.methods[names[i]]));
6354
6621
  if (json.nested)
6355
6622
  service.addJSON(json.nested);
6623
+ if (json.edition)
6624
+ service._edition = json.edition;
6356
6625
  service.comment = json.comment;
6626
+ service._defaultEdition = "proto3"; // For backwards-compatibility.
6357
6627
  return service;
6358
6628
  };
6359
6629
 
@@ -6366,6 +6636,7 @@ Service.prototype.toJSON = function toJSON(toJSONOptions) {
6366
6636
  var inherited = Namespace.prototype.toJSON.call(this, toJSONOptions);
6367
6637
  var keepComments = toJSONOptions ? Boolean(toJSONOptions.keepComments) : false;
6368
6638
  return util.toObject([
6639
+ "edition" , this._editionToJSON(),
6369
6640
  "options" , inherited && inherited.options || undefined,
6370
6641
  "methods" , Namespace.arrayToJSON(this.methodsArray, toJSONOptions) || /* istanbul ignore next */ {},
6371
6642
  "nested" , inherited && inherited.nested || undefined,
@@ -6402,6 +6673,8 @@ Service.prototype.get = function get(name) {
6402
6673
  * @override
6403
6674
  */
6404
6675
  Service.prototype.resolveAll = function resolveAll() {
6676
+ if (!this._needsRecursiveResolve) return this;
6677
+
6405
6678
  Namespace.prototype.resolve.call(this);
6406
6679
  var methods = this.methodsArray;
6407
6680
  for (var i = 0; i < methods.length; ++i)
@@ -6409,6 +6682,21 @@ Service.prototype.resolveAll = function resolveAll() {
6409
6682
  return this;
6410
6683
  };
6411
6684
 
6685
+ /**
6686
+ * @override
6687
+ */
6688
+ Service.prototype._resolveFeaturesRecursive = function _resolveFeaturesRecursive(edition) {
6689
+ if (!this._needsRecursiveFeatureResolution) return this;
6690
+
6691
+ edition = this._edition || edition;
6692
+
6693
+ Namespace.prototype._resolveFeaturesRecursive.call(this, edition);
6694
+ this.methodsArray.forEach(method => {
6695
+ method._resolveFeaturesRecursive(edition);
6696
+ });
6697
+ return this;
6698
+ };
6699
+
6412
6700
  /**
6413
6701
  * @override
6414
6702
  */
@@ -7156,6 +7444,9 @@ Type.fromJSON = function fromJSON(name, json) {
7156
7444
  type.group = true;
7157
7445
  if (json.comment)
7158
7446
  type.comment = json.comment;
7447
+ if (json.edition)
7448
+ type._edition = json.edition;
7449
+ type._defaultEdition = "proto3"; // For backwards-compatibility.
7159
7450
  return type;
7160
7451
  };
7161
7452
 
@@ -7168,6 +7459,7 @@ Type.prototype.toJSON = function toJSON(toJSONOptions) {
7168
7459
  var inherited = Namespace.prototype.toJSON.call(this, toJSONOptions);
7169
7460
  var keepComments = toJSONOptions ? Boolean(toJSONOptions.keepComments) : false;
7170
7461
  return util.toObject([
7462
+ "edition" , this._editionToJSON(),
7171
7463
  "options" , inherited && inherited.options || undefined,
7172
7464
  "oneofs" , Namespace.arrayToJSON(this.oneofsArray, toJSONOptions),
7173
7465
  "fields" , Namespace.arrayToJSON(this.fieldsArray.filter(function(obj) { return !obj.declaringField; }), toJSONOptions) || {},
@@ -7183,6 +7475,8 @@ Type.prototype.toJSON = function toJSON(toJSONOptions) {
7183
7475
  * @override
7184
7476
  */
7185
7477
  Type.prototype.resolveAll = function resolveAll() {
7478
+ if (!this._needsRecursiveResolve) return this;
7479
+
7186
7480
  Namespace.prototype.resolveAll.call(this);
7187
7481
  var oneofs = this.oneofsArray; i = 0;
7188
7482
  while (i < oneofs.length)
@@ -7193,6 +7487,24 @@ Type.prototype.resolveAll = function resolveAll() {
7193
7487
  return this;
7194
7488
  };
7195
7489
 
7490
+ /**
7491
+ * @override
7492
+ */
7493
+ Type.prototype._resolveFeaturesRecursive = function _resolveFeaturesRecursive(edition) {
7494
+ if (!this._needsRecursiveFeatureResolution) return this;
7495
+
7496
+ edition = this._edition || edition;
7497
+
7498
+ Namespace.prototype._resolveFeaturesRecursive.call(this, edition);
7499
+ this.oneofsArray.forEach(oneof => {
7500
+ oneof._resolveFeatures(edition);
7501
+ });
7502
+ this.fieldsArray.forEach(field => {
7503
+ field._resolveFeatures(edition);
7504
+ });
7505
+ return this;
7506
+ };
7507
+
7196
7508
  /**
7197
7509
  * @override
7198
7510
  */
@@ -7845,10 +8157,10 @@ util.decorateEnum = function decorateEnum(object) {
7845
8157
  * @param {Object.<string,*>} dst Destination object
7846
8158
  * @param {string} path dot '.' delimited path of the property to set
7847
8159
  * @param {Object} value the value to set
7848
- * @param {boolean} overWrite whether or not to concatenate the values into an array or overwrite; defaults to false.
8160
+ * @param {boolean|undefined} [ifNotSet] Sets the option only if it isn't currently set
7849
8161
  * @returns {Object.<string,*>} Destination object
7850
8162
  */
7851
- util.setProperty = function setProperty(dst, path, value) {
8163
+ util.setProperty = function setProperty(dst, path, value, ifNotSet) {
7852
8164
  function setProp(dst, path, value) {
7853
8165
  var part = path.shift();
7854
8166
  if (part === "__proto__" || part === "prototype") {
@@ -7858,6 +8170,8 @@ util.setProperty = function setProperty(dst, path, value) {
7858
8170
  dst[part] = setProp(dst[part] || {}, path, value);
7859
8171
  } else {
7860
8172
  var prevValue = dst[part];
8173
+ if (prevValue && ifNotSet)
8174
+ return dst;
7861
8175
  if (prevValue)
7862
8176
  value = [].concat(prevValue).concat(value);
7863
8177
  dst[part] = value;