diff --git a/cli/bin/pbjs b/cli/bin/pbjs old mode 100644 new mode 100755 diff --git a/cli/bin/pbts b/cli/bin/pbts old mode 100644 new mode 100755 diff --git a/src/object.js b/src/object.js index bf367e267..6edd99378 100644 --- a/src/object.js +++ b/src/object.js @@ -10,9 +10,10 @@ var Root; // cyclic /* eslint-disable no-warning-comments */ // TODO: Replace with embedded proto. -var editions2023Defaults = {enum_type: "OPEN", field_presence: "EXPLICIT", json_format: "ALLOW", message_encoding: "LENGTH_PREFIXED", repeated_field_encoding: "PACKED", utf8_validation: "VERIFY"}; -var proto2Defaults = {enum_type: "CLOSED", field_presence: "EXPLICIT", json_format: "LEGACY_BEST_EFFORT", message_encoding: "LENGTH_PREFIXED", repeated_field_encoding: "EXPANDED", utf8_validation: "NONE"}; -var proto3Defaults = {enum_type: "OPEN", field_presence: "IMPLICIT", json_format: "ALLOW", message_encoding: "LENGTH_PREFIXED", repeated_field_encoding: "PACKED", utf8_validation: "VERIFY"}; +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" }; +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" }; +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" }; +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" }; /** * Constructs a new reflection object instance. @@ -213,6 +214,8 @@ ReflectionObject.prototype._resolveFeatures = function _resolveFeatures(edition) defaults = Object.assign({}, proto3Defaults); } else if (edition === "2023") { defaults = Object.assign({}, editions2023Defaults); + } else if (edition === "2024") { + defaults = Object.assign({}, editions2024Defaults); } else { throw new Error("Unknown edition: " + edition); } diff --git a/src/parse.js b/src/parse.js index 469477da9..44beb87f4 100644 --- a/src/parse.js +++ b/src/parse.js @@ -261,6 +261,16 @@ function parse(source, root, options) { var token = peek(); var whichImports; switch (token) { + case "option": + if (edition < "2024") { + throw illegal("option"); + } + // Import options are only used for resolving options, which we don't + // do. We can just throw them out. + next(); + readString(); + skip(";"); + return; case "weak": whichImports = weakImports || (weakImports = []); next(); @@ -291,7 +301,7 @@ function parse(source, root, options) { function parseEdition() { skip("="); edition = readString(); - const supportedEditions = ["2023"]; + const supportedEditions = ["2023", "2024"]; /* istanbul ignore if */ if (!supportedEditions.includes(edition)) @@ -317,6 +327,21 @@ function parse(source, root, options) { parseEnum(parent, token); return true; + case "export": + case "local": + if (edition < "2024") { + return false; + } + token = next(); + if (token === "export" || token === "local") { + return false; + } + if (token !== "message" && token !== "enum") { + return false; + } + // TODO: actually enforce visiblity modifiers like protoc does. + return parseCommon(parent, token); + case "service": parseService(parent, token); return true; @@ -523,6 +548,24 @@ function parse(source, root, options) { parseEnum(type, token); break; + case "export": + case "local": + if (edition < "2024") { + throw illegal(token); + } + token = next(); + switch (token) { + case "message": + parseType(type, token); + break; + case "enum": + parseType(type, token); + break; + default: + throw illegal(token); + } + break; + /* istanbul ignore next */ default: throw illegal(token); // there are no groups with proto3 semantics diff --git a/tests/data/import-option-bad.proto b/tests/data/import-option-bad.proto new file mode 100644 index 000000000..299889861 --- /dev/null +++ b/tests/data/import-option-bad.proto @@ -0,0 +1,9 @@ +edition = "2024"; + +import option "nonexistent.proto"; + +option features.(pb.nonexistent).foo = BAR; + +message Foo { + string legacy = 1; +} \ No newline at end of file diff --git a/tests/feature_resolution_editions.js b/tests/feature_resolution_editions.js index edf7db9c8..64b9ebcd9 100644 --- a/tests/feature_resolution_editions.js +++ b/tests/feature_resolution_editions.js @@ -66,20 +66,22 @@ var tape = require("tape"); var protobuf = require(".."); +var protoEditions2024 = `edition = "2024"; message Foo {}`; var protoEditions2023 = `edition = "2023"; message Foo {}`; var proto2 = `syntax = "proto2"; message Foo {}`; var proto3 = `syntax = "proto3"; message Foo {}`; -var editions2023Defaults = {enum_type: 'OPEN', field_presence: 'EXPLICIT', json_format: 'ALLOW', message_encoding: 'LENGTH_PREFIXED', repeated_field_encoding: 'PACKED', utf8_validation: 'VERIFY'} -var proto2Defaults = {enum_type: 'CLOSED', field_presence: 'EXPLICIT', json_format: 'LEGACY_BEST_EFFORT', message_encoding: 'LENGTH_PREFIXED', repeated_field_encoding: 'EXPANDED', utf8_validation: 'NONE'} -var proto3Defaults = {enum_type: 'OPEN', field_presence: 'IMPLICIT', json_format: 'ALLOW', message_encoding: 'LENGTH_PREFIXED', repeated_field_encoding: 'PACKED', utf8_validation: 'VERIFY'} +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" }; +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" }; +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" }; +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" }; tape.test("feature resolution defaults", function(test) { - var rootEditions = protobuf.parse(protoEditions2023).root; + var rootEditions = protobuf.parse(protoEditions2024).root; rootEditions.resolveAll(); - test.same(rootEditions.Foo._features, editions2023Defaults); + test.same(rootEditions.Foo._features, editions2024Defaults); var rootProto2 = protobuf.parse(proto2).root; rootProto2.resolveAll(); @@ -136,6 +138,8 @@ tape.test("aggregate feature parsing", function(test) { message_encoding: 'LENGTH_PREFIXED', repeated_field_encoding: 'PACKED', utf8_validation: 'NONE', + enforce_naming_style: "STYLE_LEGACY", + default_symbol_visibility: "EXPORT_ALL" }) test.end(); @@ -160,6 +164,8 @@ tape.test("feature resolution inheritance file to message", function(test) { message_encoding: 'LENGTH_PREFIXED', repeated_field_encoding: 'PACKED', utf8_validation: 'VERIFY', + enforce_naming_style: "STYLE_LEGACY", + default_symbol_visibility: "EXPORT_ALL", '(abc)': { d_e: 'deeply_nested_false' } }) @@ -185,6 +191,8 @@ tape.test("feature resolution inheritance message to field", function(test) { message_encoding: 'LENGTH_PREFIXED', repeated_field_encoding: 'PACKED', utf8_validation: 'VERIFY', + enforce_naming_style: "STYLE_LEGACY", + default_symbol_visibility: "EXPORT_ALL", '(abc)': { d_e: 'deeply_nested_false' } }) @@ -213,6 +221,8 @@ tape.test("feature resolution inheritance message to nested message", function(t message_encoding: 'LENGTH_PREFIXED', repeated_field_encoding: 'PACKED', utf8_validation: 'VERIFY', + enforce_naming_style: "STYLE_LEGACY", + default_symbol_visibility: "EXPORT_ALL", '(abc)': { d_e: 'deeply_nested_true' } }); test.end(); @@ -238,6 +248,8 @@ tape.test("feature resolution inheritance enum to enum value", function(test) { message_encoding: 'LENGTH_PREFIXED', repeated_field_encoding: 'EXPANDED', utf8_validation: 'VERIFY', + enforce_naming_style: "STYLE_LEGACY", + default_symbol_visibility: "EXPORT_ALL", '(abc)': { d_e: 'deeply_nested_false' } }) @@ -248,7 +260,9 @@ tape.test("feature resolution inheritance enum to enum value", function(test) { message_encoding: 'LENGTH_PREFIXED', repeated_field_encoding: 'PACKED', utf8_validation: 'VERIFY', - '(abc)': { d_e: 'deeply_nested_false' } + enforce_naming_style: "STYLE_LEGACY", + default_symbol_visibility: "EXPORT_ALL", + '(abc)': { d_e: 'deeply_nested_false' } }); test.end(); @@ -274,6 +288,8 @@ tape.test("feature resolution inheritance message to oneofs", function(test) { message_encoding: 'LENGTH_PREFIXED', repeated_field_encoding: 'PACKED', utf8_validation: 'VERIFY', + enforce_naming_style: "STYLE_LEGACY", + default_symbol_visibility: "EXPORT_ALL", '(abc)': { d_e: 'deeply_nested_false' } }) @@ -301,7 +317,9 @@ tape.test("feature resolution inheritance oneofs", function(test) { message_encoding: 'LENGTH_PREFIXED', repeated_field_encoding: 'PACKED', utf8_validation: 'VERIFY', - '(abc)': { d_e: 'deeply_nested_false' } + enforce_naming_style: "STYLE_LEGACY", + default_symbol_visibility: "EXPORT_ALL", + '(abc)': { d_e: 'deeply_nested_false' } }) test.same(rootEditionsOverriden.lookup("SomeOneOf").fieldsArray.find(x => x.name === 'b')._features, { @@ -311,6 +329,8 @@ tape.test("feature resolution inheritance oneofs", function(test) { message_encoding: 'LENGTH_PREFIXED', repeated_field_encoding: 'PACKED', utf8_validation: 'VERIFY', + enforce_naming_style: "STYLE_LEGACY", + default_symbol_visibility: "EXPORT_ALL", '(abc)': { d_e: 'deeply_nested_false' } }) @@ -338,6 +358,8 @@ tape.test("feature resolution inheritance file to extensions", function(test) { message_encoding: 'LENGTH_PREFIXED', repeated_field_encoding: 'PACKED', utf8_validation: 'NONE', + enforce_naming_style: "STYLE_LEGACY", + default_symbol_visibility: "EXPORT_ALL", '(abc)': { d_e: 'deeply_nested_false' } }) @@ -365,6 +387,8 @@ tape.test("feature resolution inheritance message to extensions", function(test) message_encoding: 'LENGTH_PREFIXED', repeated_field_encoding: 'PACKED', utf8_validation: 'NONE', + enforce_naming_style: "STYLE_LEGACY", + default_symbol_visibility: "EXPORT_ALL", '(abc)': { d_e: 'deeply_nested_false' } }) @@ -391,6 +415,8 @@ tape.test("feature resolution inheritance message to enum", function(test) { message_encoding: 'LENGTH_PREFIXED', repeated_field_encoding: 'PACKED', utf8_validation: 'NONE', + enforce_naming_style: "STYLE_LEGACY", + default_symbol_visibility: "EXPORT_ALL", '(abc)': { d_e: 'deeply_nested_false' } }) @@ -401,6 +427,8 @@ tape.test("feature resolution inheritance message to enum", function(test) { message_encoding: 'LENGTH_PREFIXED', repeated_field_encoding: 'PACKED', utf8_validation: 'NONE', + enforce_naming_style: "STYLE_LEGACY", + default_symbol_visibility: "EXPORT_ALL", '(abc)': { d_e: 'deeply_nested_false' } }) @@ -425,6 +453,8 @@ tape.test("feature resolution inheritance file to enum", function(test) { message_encoding: 'LENGTH_PREFIXED', repeated_field_encoding: 'PACKED', utf8_validation: 'NONE', + enforce_naming_style: "STYLE_LEGACY", + default_symbol_visibility: "EXPORT_ALL", '(abc)': { d_e: 'deeply_nested_false' } }) @@ -451,6 +481,8 @@ tape.test("feature resolution inheritance file to service and service to method" message_encoding: 'LENGTH_PREFIXED', repeated_field_encoding: 'PACKED', utf8_validation: 'VERIFY', + enforce_naming_style: "STYLE_LEGACY", + default_symbol_visibility: "EXPORT_ALL", '(abc)': { d_e: 'deeply_nested_false' } }) @@ -461,6 +493,8 @@ tape.test("feature resolution inheritance file to service and service to method" message_encoding: 'LENGTH_PREFIXED', repeated_field_encoding: 'PACKED', utf8_validation: 'NONE', + enforce_naming_style: "STYLE_LEGACY", + default_symbol_visibility: "EXPORT_ALL", '(abc)': { d_e: 'deeply_nested_false' } }) @@ -480,6 +514,8 @@ tape.test("feature resolution editions precedence", function(test) { message_encoding: 'LENGTH_PREFIXED', repeated_field_encoding: 'PACKED', utf8_validation: 'VERIFY', + enforce_naming_style: "STYLE_LEGACY", + default_symbol_visibility: "EXPORT_ALL", amazing_feature: 'G' }) test.end(); diff --git a/tests/parse_editions.js b/tests/parse_editions.js new file mode 100644 index 000000000..f90c12bc8 --- /dev/null +++ b/tests/parse_editions.js @@ -0,0 +1,159 @@ +var tape = require("tape"); + +var protobuf = require(".."); + + +tape.test("invalid edition", function(test) { + test.throws(function() { + protobuf.parse(`edition = "2022"; message A {}`); + }, Error, /Error: illegal edition '2022'/, "unknown past edition should be rejected"); + + test.throws(function() { + protobuf.parse(`edition = "2030"; message A {}`); + }, Error, /Error: illegal edition '2030'/, "unknown future edition should be rejected"); + + test.end(); +}); + +tape.test("edition 2023 banned keywords", function(test) { + test.throws(function() { + protobuf.parse(`edition = "2023"; + message A {\ + required uint32 a = 1;\ + }`); + }, Error, /Error: illegal token 'required'/, "required should be banned"); + + test.throws(function() { + protobuf.parse(`edition = "2023"; + message A {\ + optional uint32 a = 1;\ + }`); + }, Error, /Error: illegal token 'optional'/, "optional should be banned"); + + test.throws(function() { + protobuf.parse(`edition = "2023"; + message A {\ + group uint32 a = 1;\ + }`); + }, Error, /Error: illegal token 'group'/); + + test.end(); +}); + +tape.test("edition 2023 reserved", function(test) { + var root = protobuf.parse(`edition = "2023"; + message Foo { + reserved bar, baz; + }`).root.resolveAll(); + test.same(root.Foo.reserved, ["bar", "baz"], "reserved fields should be parsed"); + + root = protobuf.parse(`edition = "2023"; + enum Foo { + reserved BAR, BAZ_BAZ; + }`).root.resolveAll(); + test.same(root.nested.Foo.reserved, ["BAR", "BAZ_BAZ"], "reserved values should be parsed"); + + test.throws(function() { + protobuf.parse(`edition = "2023"; + message Foo { + reserved "bar", "baz"; + }`); + }, /Error: illegal id 'bar'/, "reserved field strings should be banned"); + + test.throws(function() { + protobuf.parse(`edition = "2023"; + enum Foo { + reserved "BAR", "BAZ"; + }`); + }, /Error: illegal id 'BAR'/, "reserved enum value strings should be banned"); + + test.throws(function() { + protobuf.parse(`syntax = "proto3"; + message Foo { + reserved bar, baz; + }`); + }, /Error: illegal id 'bar'/, "reserved field strings should be banned"); + + test.throws(function() { + protobuf.parse(`syntax = "proto3"; + enum Foo { + reserved BAR, BAZ; + }`); + }, /Error: illegal id 'BAR'/, "reserved enum value strings should be banned"); + + test.end(); +}); + +tape.test("edition 2024 visibility", function(test) { + test.ok(protobuf.parse(`edition = "2024"; export message Foo {}`), "messages should allow export modifier");; + test.ok(protobuf.parse(`edition = "2024"; export enum Foo {}`), "enums should allow export modifier");; + test.ok(protobuf.parse(`edition = "2024"; local message Foo {}`), "messages should allow local modifier");; + test.ok(protobuf.parse(`edition = "2024"; local enum Foo {}`), "enums should allow local modifier");; + test.ok(protobuf.parse(`edition = "2024"; + message Foo { + export message Export {} + local message Local {} + }`), "nested messages should allow visibility modifiers");; + test.ok(protobuf.parse(`edition = "2024"; + message Foo { + export enum Export {} + local enum Local {} + }`), "nested enums should allow visibility modifiers");; + + test.throws(function() { + protobuf.parse(`edition = "2023"; export message Foo {}`) + }, /Error: illegal token 'export'/, "export should be banned before edition 2024"); + test.throws(function() { + protobuf.parse(`edition = "2024"; export service Foo {}`) + }, /Error: illegal token 'export'/, "export should be banned on services"); + test.throws(function() { + protobuf.parse(`edition = "2024"; + message Empty {} + service Foo { + export rpc Method(Empty) returns (Empty); + }`) + }, /Error: illegal token 'export'/, "export should be banned on methods"); + test.throws(function() { + protobuf.parse(`edition = "2024"; export option foo = 1;`) + }, /Error: illegal token 'export'/, "export should be banned on options"); + + test.throws(function() { + protobuf.parse(`edition = "2023"; local message Foo {}`) + }, /Error: illegal token 'local'/, "local should be banned before edition 2024"); + test.throws(function() { + protobuf.parse(`edition = "2024"; local service Foo {}`) + }, /Error: illegal token 'local'/, "local should be banned on services"); + test.throws(function() { + protobuf.parse(`edition = "2024"; + message Empty {} + service Foo { + local rpc Method(Empty) returns (Empty); + }`) + }, /Error: illegal token 'local'/, "local should be banned on methods"); + test.throws(function() { + protobuf.parse(`edition = "2024"; local option foo = 1;`) + }, /Error: illegal token 'local'/, "local should be banned on options"); + + test.end(); +}); + + +tape.test("edition 2024 import option", function(test) { + test.same(protobuf.parse(`edition = "2024"; import "foo.proto";`).imports, ["foo.proto"], "regular options should fetch"); + test.equals(protobuf.parse(`edition = "2024"; import option "foo.proto";`).imports, undefined, "import option should not fetch"); + test.same(protobuf.parse(`edition = "2024"; + import option "foo.proto"; + import "bar.proto"; + import option "foo2.proto"; + `).imports, ["bar.proto"], "multiple import options should not fetch"); + + test.throws(function() { + protobuf.parse(`edition = "2023"; import option "foo.proto";`); + }, /Error: illegal token 'option'/, "import option should be banned before edition 2024"); + + var root = new protobuf.Root(); + root.loadSync("tests/data/import-option-bad.proto"); + root.resolveAll(); + + test.end(); +});