Skip to content

Commit 714c8e1

Browse files
Partially fix some bugs handling the load of cross-edition files.
This will now work as long as all files sharing a package agree on file-level edition and options
1 parent 60f3e51 commit 714c8e1

File tree

6 files changed

+188
-35
lines changed

6 files changed

+188
-35
lines changed

cli/targets/proto.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ function buildRoot(root) {
102102
if (pkg.length)
103103
out.push("", "package " + pkg.join(".") + ";");
104104

105-
buildOptions(ptr, ["edition", "syntax"]);
105+
buildOptions(ptr, ["edition"]);
106106
ptr.nestedArray.forEach(build);
107107
}
108108

src/index-light.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ protobuf.types = require("./types");
9898
protobuf.util = require("./util");
9999

100100
// Set up possibly cyclic reflection dependencies
101-
protobuf.ReflectionObject._configure(protobuf.Root);
101+
protobuf.ReflectionObject._configure(protobuf.Root, protobuf.Namespace);
102102
protobuf.Namespace._configure(protobuf.Type, protobuf.Service, protobuf.Enum);
103103
protobuf.Root._configure(protobuf.Type);
104104
protobuf.Field._configure(protobuf.Type);

src/object.js

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ ReflectionObject.className = "ReflectionObject";
66
const OneOf = require("./oneof");
77
var util = require("./util");
88

9-
var Root; // cyclic
9+
var Root, Namespace; // cyclic
1010

1111
/* eslint-disable no-warning-comments */
1212
// TODO: Replace with embedded proto.
@@ -163,7 +163,8 @@ ReflectionObject.prototype.onRemove = function onRemove(parent) {
163163
ReflectionObject.prototype.resolve = function resolve() {
164164
if (this.resolved)
165165
return this;
166-
if (this instanceof Root || this.parent && this.parent.resolved) {
166+
var edition = this.getOption("edition");
167+
if ((this instanceof Namespace && edition) || (this.parent && this.parent.resolved)) {
167168
this._resolveFeatures();
168169
this.resolved = true;
169170
}
@@ -177,24 +178,27 @@ ReflectionObject.prototype.resolve = function resolve() {
177178
ReflectionObject.prototype._resolveFeatures = function _resolveFeatures() {
178179
var defaults = {};
179180

180-
var edition = this.root.getOption("edition");
181-
if (this instanceof Root) {
182-
if (this.root.getOption("syntax") === "proto3") {
181+
var protoFeatures = Object.assign(Object.assign({}, this._protoFeatures), this._inferLegacyProtoFeatures(edition));
182+
183+
var edition = this.getOption("edition");
184+
if (this instanceof Namespace && edition) {
185+
// For a namespace marked with a specific edition, reset defaults.
186+
if (edition === "proto2") {
187+
defaults = Object.assign({}, proto2Defaults);
188+
} else if (edition === "proto3") {
183189
defaults = Object.assign({}, proto3Defaults);
184190
} else if (edition === "2023") {
185191
defaults = Object.assign({}, editions2023Defaults);
186192
} else {
187-
defaults = Object.assign({}, proto2Defaults);
193+
throw new Error("Unknown edition: " + edition);
188194
}
195+
this._features = Object.assign(defaults, protoFeatures || {});
196+
return;
189197
}
190198

191-
var protoFeatures = Object.assign(Object.assign({}, this._protoFeatures), this._inferLegacyProtoFeatures(edition));
192-
193-
if (this instanceof Root) {
194-
this._features = Object.assign(defaults, protoFeatures || {});
195199
// fields in Oneofs aren't actually children of them, so we have to
196200
// special-case it
197-
} else if (this.partOf instanceof OneOf) {
201+
if (this.partOf instanceof OneOf) {
198202
var lexicalParentFeaturesCopy = Object.assign({}, this.partOf._features);
199203
this._features = Object.assign(lexicalParentFeaturesCopy, protoFeatures || {});
200204
} else if (this.declaringField) {
@@ -241,8 +245,10 @@ ReflectionObject.prototype.getOption = function getOption(name) {
241245
* @returns {ReflectionObject} `this`
242246
*/
243247
ReflectionObject.prototype.setOption = function setOption(name, value, ifNotSet) {
244-
if (!ifNotSet || !this.options || this.options[name] === undefined)
248+
if (!ifNotSet || !this.options || this.options[name] === undefined) {
249+
if (this.getOption(name) !== value) this.resolved = false;
245250
(this.options || (this.options = {}))[name] = value;
251+
}
246252
return this;
247253
};
248254

@@ -318,6 +324,7 @@ ReflectionObject.prototype.toString = function toString() {
318324
};
319325

320326
// Sets up cyclic dependencies (called in index-light)
321-
ReflectionObject._configure = function(Root_) {
327+
ReflectionObject._configure = function(Root_, Namespace_) {
322328
Root = Root_;
329+
Namespace = Namespace_;
323330
};

src/parse.js

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ var base10Re = /^[1-9][0-9]*$/,
3333
* @property {string|undefined} package Package name, if declared
3434
* @property {string[]|undefined} imports Imports, if any
3535
* @property {string[]|undefined} weakImports Weak imports, if any
36-
* @property {string|undefined} syntax Syntax, if specified (either `"proto2"` or `"proto3"`)
3736
* @property {Root} root Populated root instance
3837
*/
3938

@@ -81,9 +80,9 @@ function parse(source, root, options) {
8180
pkg,
8281
imports,
8382
weakImports,
84-
syntax,
85-
edition = false,
86-
isProto3 = false;
83+
edition = "proto2",
84+
isProto3 = false,
85+
isProto2 = true;
8786

8887
var ptr = root;
8988

@@ -145,7 +144,7 @@ function parse(source, root, options) {
145144
try {
146145
target.push([ start = parseId(next()), skip("to", true) ? parseId(next()) : start ]);
147146
} catch (err) {
148-
if (typeRefRe.test(token) && edition) {
147+
if (typeRefRe.test(token) && (!isProto2 && !isProto3)) {
149148
target.push(token);
150149
} else {
151150
throw err;
@@ -228,7 +227,6 @@ function parse(source, root, options) {
228227
}
229228

230229
function parsePackage() {
231-
232230
/* istanbul ignore if */
233231
if (pkg !== undefined)
234232
throw illegal("package");
@@ -240,6 +238,7 @@ function parse(source, root, options) {
240238
throw illegal(pkg, "name");
241239

242240
ptr = ptr.define(pkg);
241+
ptr.setOption("edition", edition);
243242
skip(";");
244243
}
245244

@@ -265,23 +264,26 @@ function parse(source, root, options) {
265264

266265
function parseSyntax() {
267266
skip("=");
268-
syntax = readString();
269-
isProto3 = syntax === "proto3";
267+
edition = readString();
268+
isProto3 = edition === "proto3";
269+
isProto2 = edition === "proto2";
270270

271271
/* istanbul ignore if */
272-
if (!isProto3 && syntax !== "proto2")
273-
throw illegal(syntax, "syntax");
272+
if (!isProto3 && !isProto2)
273+
throw illegal(edition, "syntax");
274274

275275
// Syntax is needed to understand the meaning of the optional field rule
276276
// Otherwise the meaning is ambiguous between proto2 and proto3
277-
root.setOption("syntax", syntax);
277+
root.setOption("edition", edition);
278278

279279
skip(";");
280280
}
281281

282282
function parseEdition() {
283283
skip("=");
284284
edition = readString();
285+
isProto3 = false;
286+
isProto2 = false;
285287
const supportedEditions = ["2023"];
286288

287289
/* istanbul ignore if */
@@ -361,7 +363,7 @@ function parse(source, root, options) {
361363
break;
362364

363365
case "required":
364-
if (edition)
366+
if (!isProto2)
365367
throw illegal(token);
366368
/* eslint-disable no-fallthrough */
367369
case "repeated":
@@ -372,7 +374,7 @@ function parse(source, root, options) {
372374
/* istanbul ignore if */
373375
if (isProto3) {
374376
parseField(type, "proto3_optional");
375-
} else if (edition) {
377+
} else if (!isProto2) {
376378
throw illegal(token);
377379
} else {
378380
parseField(type, "optional");
@@ -393,7 +395,7 @@ function parse(source, root, options) {
393395

394396
default:
395397
/* istanbul ignore if */
396-
if (!isProto3 && !edition || !typeRefRe.test(token)) {
398+
if (isProto2 || !typeRefRe.test(token)) {
397399
throw illegal(token);
398400
}
399401

@@ -854,7 +856,7 @@ function parse(source, root, options) {
854856

855857
default:
856858
/* istanbul ignore if */
857-
if (!isProto3 && !edition || !typeRefRe.test(token))
859+
if (isProto2 || !typeRefRe.test(token))
858860
throw illegal(token);
859861
push(token);
860862
parseField(parent, "optional", reference);
@@ -924,7 +926,6 @@ function parse(source, root, options) {
924926
"package" : pkg,
925927
"imports" : imports,
926928
weakImports : weakImports,
927-
syntax : syntax,
928929
root : root
929930
};
930931
}

src/root.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ function Root(options) {
3535
* @type {string[]}
3636
*/
3737
this.files = [];
38+
39+
// Default to proto2 if not specified.
40+
this.setOption("edition", "proto2", true);
3841
}
3942

4043
/**

tests/feature_resolution_editions.js

Lines changed: 146 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -448,12 +448,12 @@ tape.test("feature resolution editions precedence", function(test) {
448448

449449
test.same(root.lookup("Message").lookupEnum("SomeEnumInMessage")._features,
450450
{
451-
enum_type: 'CLOSED',
451+
enum_type: 'OPEN',
452452
field_presence: 'EXPLICIT',
453-
json_format: 'LEGACY_BEST_EFFORT',
453+
json_format: 'ALLOW',
454454
message_encoding: 'LENGTH_PREFIXED',
455-
repeated_field_encoding: 'EXPANDED',
456-
utf8_validation: 'NONE',
455+
repeated_field_encoding: 'PACKED',
456+
utf8_validation: 'VERIFY',
457457
amazing_feature: 'G'
458458
})
459459
test.end();
@@ -547,3 +547,145 @@ tape.test("feature resolution inferred proto2 presence", function(test) {
547547

548548
test.end();
549549
});
550+
551+
tape.test("feature resolution mixed syntax different package", function(test) {
552+
var root = protobuf.parse(`syntax = "proto2";
553+
package proto2;
554+
message Message {
555+
optional int32 default = 1;
556+
required int32 required = 2;
557+
repeated int32 unpacked = 3;
558+
}`).root;
559+
protobuf.parse(`syntax = "proto3";
560+
package proto3;
561+
message Message {
562+
optional int32 explicit = 1;
563+
int32 implicit = 2;
564+
repeated int32 packed = 3;
565+
}`, root);
566+
root.resolveAll();
567+
568+
var proto2 = root.lookup("proto2.Message");
569+
test.ok(proto2.fields.default.hasPresence, "proto2 uses explicit presence");
570+
test.ok(proto2.fields.required.required, "proto2 has required fields");
571+
test.notOk(proto2.fields.unpacked.packed, "proto2 is expanded by default");
572+
573+
var proto3 = root.lookup("proto3.Message");
574+
test.ok(proto3.fields.explicit.hasPresence, "proto3 optional has explicit presence");
575+
test.notOk(proto3.fields.implicit.hasPresence, "proto3 is implicit presence by default");
576+
test.ok(proto3.fields.packed.packed, "proto3 is packed by default");
577+
578+
test.end();
579+
});
580+
581+
tape.test("feature resolution mixed file features different package", function(test) {
582+
var root = protobuf.parse(`edition = "2023";
583+
package expanded;
584+
option features.repeated_field_encoding = EXPANDED;
585+
message Message {
586+
repeated int32 expanded = 1;
587+
repeated int32 packed = 2 [features.repeated_field_encoding = PACKED];
588+
}`).root;
589+
protobuf.parse(`edition = "2023";
590+
package packed;
591+
option features.repeated_field_encoding = PACKED;
592+
message Message {
593+
repeated int32 packed = 1;
594+
repeated int32 expanded = 2 [features.repeated_field_encoding = EXPANDED];
595+
}`, root);
596+
root.resolveAll();
597+
598+
var expanded = root.lookup("expanded.Message");
599+
test.notOk(expanded.fields.expanded.packed, "expanded by default");
600+
test.ok(expanded.fields.packed.packed, "packed override");
601+
602+
var packed = root.lookup("packed.Message");
603+
test.ok(packed.fields.packed.packed, "packed by default");
604+
test.notOk(packed.fields.expanded.packed, "expanded override");
605+
606+
test.end();
607+
});
608+
609+
/*
610+
// These cases are bugged, because they dump file features into the same namespace!
611+
612+
tape.test("feature resolution mixed file features same package", function(test) {
613+
var root = protobuf.parse(`edition = "2023";
614+
option features.repeated_field_encoding = EXPANDED;
615+
message Message1 {
616+
repeated int32 expanded = 1;
617+
int32 explicit = 2;
618+
}`).root;
619+
protobuf.parse(`edition = "2023";
620+
option features.field_presence = IMPLICIT;
621+
message Message2 {
622+
repeated int32 packed = 1;
623+
int32 implicit = 3;
624+
}`, root);
625+
root.resolveAll();
626+
627+
var msg1 = root.lookup("Message1");
628+
test.notOk(msg1.fields.expanded.packed, "expanded by default");
629+
test.ok(msg1.fields.explicit.hasPresence, "explicit by default");
630+
631+
var msg2 = root.lookup("Message2");
632+
test.ok(msg2.fields.packed.packed, "packed by default");
633+
test.notOk(msg2.fields.implicit.hasPresence, "implicit by default");
634+
635+
test.end();
636+
});
637+
638+
tape.test("feature resolution mixed file features same package", function(test) {
639+
var root = protobuf.parse(`edition = "2023";
640+
option features.repeated_field_encoding = EXPANDED;
641+
message Message1 {
642+
repeated int32 expanded = 1;
643+
repeated int32 packed = 2 [features.repeated_field_encoding = PACKED];
644+
}`).root;
645+
protobuf.parse(`edition = "2023";
646+
option features.repeated_field_encoding = PACKED;
647+
message Message2 {
648+
repeated int32 packed = 1;
649+
repeated int32 expanded = 2 [features.repeated_field_encoding = EXPANDED];
650+
}`, root);
651+
root.resolveAll();
652+
653+
var expanded = root.lookup("Message1");
654+
test.notOk(expanded.fields.expanded.packed, "expanded by default");
655+
test.ok(expanded.fields.packed.packed, "packed override");
656+
657+
var packed = root.lookup("Message2");
658+
test.ok(packed.fields.packed.packed, "packed by default");
659+
test.notOk(packed.fields.expanded.packed, "expanded override");
660+
661+
test.end();
662+
});
663+
664+
tape.test("feature resolution mixed syntax same package", function(test) {
665+
var root = protobuf.parse(`syntax = "proto2";
666+
message Message2 {
667+
optional int32 default = 1;
668+
required int32 required = 2;
669+
repeated int32 unpacked = 3;
670+
}`).root;
671+
protobuf.parse(`syntax = "proto3";
672+
message Message3 {
673+
optional int32 explicit = 1;
674+
int32 implicit = 2;
675+
repeated int32 packed = 3;
676+
}`, root);
677+
root.resolveAll();
678+
679+
var proto2 = root.lookup("Message2");
680+
test.ok(proto2.fields.default.hasPresence, "proto2 uses explicit presence");
681+
test.ok(proto2.fields.required.required, "proto2 has required fields");
682+
test.notOk(proto2.fields.unpacked.packed, "proto2 is expanded by default");
683+
684+
var proto3 = root.lookup("Message3");
685+
test.ok(proto3.fields.explicit.hasPresence, "proto3 optional has explicit presence");
686+
test.notOk(proto3.fields.implicit.hasPresence, "proto3 is implicit presence by default");
687+
test.ok(proto3.fields.packed.packed, "proto3 is packed by default");
688+
689+
test.end();
690+
});
691+
*/

0 commit comments

Comments
 (0)