Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add feature resolution for protobuf editions #2029

Open
wants to merge 33 commits into
base: master
Choose a base branch
from

Conversation

sofisl
Copy link

@sofisl sofisl commented Sep 10, 2024

No description provided.

tests/feature_resolution_editions.js Outdated Show resolved Hide resolved
src/parse.js Outdated Show resolved Hide resolved
src/parse.js Show resolved Hide resolved
src/parse.js Outdated Show resolved Hide resolved
src/object.js Outdated Show resolved Hide resolved
src/object.js Outdated Show resolved Hide resolved
tests/feature_resolution_editions.js Outdated Show resolved Hide resolved
src/parse.js Show resolved Hide resolved
@@ -30,7 +30,7 @@ message Message {
tape.test("complex options", function (test) {
var root = protobuf.parse(proto).root;

test.deepEqual(root.parsedOptions[0], {
test.deepEqual(root.parsedOptions[1], {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we now have defaults as the first parsed option, this test needs to look for the second parsed option.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to put defaults into "parsedOptions"? That seems like the wrong place to store them, since they're global

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parsedOptions are just options that have been properly parsed - in other words, a step after options. Since features are just options, and they need to be parsed just like options, they do need to go through this step. If you want I can delete them from options after they've been copied over into _features.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My confusion is why defaults end up here. Parsed features make sense to me, but you're explicitly adding edition defaults to parsedOptions

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha - yeah it seems it is related. I think the point is that we're taking advantage of the parsing setParsedOptions offers, which ultimately is needed in feature resolution. If we move it outside of setting parsedOptions into some other feature resolution-specific method, we're essentially copying over reusable code. Same reason why we're caling setParsedOption here too, since it takes advantage of all the parsing already offered in that method.

tests/api_object.js Outdated Show resolved Hide resolved
src/object.js Outdated
@@ -186,6 +207,7 @@ ReflectionObject.prototype.setParsedOption = function setParsedOption(name, valu
if (!this.parsedOptions) {
this.parsedOptions = [];
}
var isFeature = /features/.test(name);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like it might have some false positive. Do we want to make sure that it starts with features.?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you're still missing an ., this would pass for an option named featuresFoo

@@ -195,12 +217,13 @@ ReflectionObject.prototype.setParsedOption = function setParsedOption(name, valu
});
if (opt) {
// If we found an existing option - just merge the property value
// (If it's a feature, will just write over)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to special-case features? They should all be scalar so we should never be trying to merge during parse

Copy link
Author

@sofisl sofisl Sep 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feature really only gets called when parsing Root, since root gets initialized with defaults, and then can be overwritten if there's options specified at the root level. For example, if we start off with editions syntax, and then immediately add the following at the root level:

option features.json_format = LEGACY_BEST_EFFORT;

Since the previous experience is to merge at each child, then the features-specific behavior gets lost, since we'd end up with an array of values (features.json_format = [LEGACY_BEST_EFFORT, ALLOW]);

One option is to simply initialize defaults at the time that we set root-level options, so there's no overwriting, just 'filling in the blanks'. However this would require much of the same logic that setParsedOption already does (i.e., searching for a property within the_proto_features object), and it seems pretty un-DRY to rewrite it for features only because the setProperty value is different.

Instead, I'll change the isFeature parameter to overwrite in setProperty to make it more general.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sounds related to my confusion about calling setParsedOption with defaults. Alternatively, you could just initialize _features of the root to the relevant defaults during feature resolution right? i.e. if _parent is null, initialize with the edition defaults

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, but what if parent isn't null? In that case we're rewriting a lot of the code that is in setParsedOption to confirm whether or not a value exists and if to rewrite it, or to create a new property, etc.

src/object.js Outdated Show resolved Hide resolved
src/parse.js Outdated Show resolved Hide resolved
src/parse.js Show resolved Hide resolved
src/parse.js Outdated
@@ -338,13 +371,17 @@ function parse(source, root, options) {

case "required":
case "repeated":
if (edition)
throw illegal(token);
parseField(type, token);
break;

case "optional":
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In addition to optional and required, we have the following grammar changes I don't see covered:

  • "group" keyword is removed
  • reserved field and enum value names are specified as identifiers, rather than string literals (i.e. no quotes)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added tests for the grammar changes. As for the second point, I believe this is already covered by feature resolution tests since there are no quotes.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that second case is covered, since you have no reserved fields. What I'm talking about is that for a proto2 message:

message Foo {
  reserved "bar", "baz";
}

becomes this in edition 2023:

message Foo {
  reserved bar, baz;
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added tests.

src/parse.js Outdated
dummy.setParsedOption = function(name, value, propName) {
// In order to not change existing behavior, only calling
// this for features
if (/features/.test(name)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar issue with this regex I think

tests/api_object.js Outdated Show resolved Hide resolved
@@ -30,7 +30,7 @@ message Message {
tape.test("complex options", function (test) {
var root = protobuf.parse(proto).root;

test.deepEqual(root.parsedOptions[0], {
test.deepEqual(root.parsedOptions[1], {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to put defaults into "parsedOptions"? That seems like the wrong place to store them, since they're global

// Tests precedence for different levels of feature resolution
tape.test("feature resolution editions precedence", function(test) {

protobuf.load("tests/data/feature-resolution.proto", function(err, root) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it might be helpful to split up this proto into smaller ones, and define them inline per-test. I regret using monolithic protos in our tests, as it's much harder to understand what's being tests

Copy link
Author

@sofisl sofisl Sep 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hear you, but I think in this case it helps the test reader understand the overall precedence of options - at least it does for me. It's clear by the alphabetical order what has precedence in the grand scheme of things, rather than having to piece it together in each test. As long as it doesn't get used for other development, I think it's helpful to get the greater overall picture of precedence.

Copy link
Contributor

@mkruskal-google mkruskal-google Sep 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is that to understand this test you need to constantly be switching back and forth between two different files (as a reader, I find this test difficult to review for correctness). If you had much smaller protos defined inline in each test, it would be much clearer what the expectation is

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, switched it.

src/object.js Outdated
if (this.root instanceof Root)
this.resolved = true; // only if part of a root
this._resolveFeatures();
if (this.root instanceof Root || this.parent)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this sufficient?

  • We probably don't want to call _resolveFeatures() unless this condition is true
  • What if the parent is itself orphaned?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Moved _resolveFeatures, I think you're right.
  • If the parent itself is orphaned, then this wouldn't get called recursively since this usually only gets called under resolveAll.

src/object.js Outdated
@@ -186,6 +207,7 @@ ReflectionObject.prototype.setParsedOption = function setParsedOption(name, valu
if (!this.parsedOptions) {
this.parsedOptions = [];
}
var isFeature = /features/.test(name);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you're still missing an ., this would pass for an option named featuresFoo

@@ -195,12 +217,13 @@ ReflectionObject.prototype.setParsedOption = function setParsedOption(name, valu
});
if (opt) {
// If we found an existing option - just merge the property value
// (If it's a feature, will just write over)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sounds related to my confusion about calling setParsedOption with defaults. Alternatively, you could just initialize _features of the root to the relevant defaults during feature resolution right? i.e. if _parent is null, initialize with the edition defaults

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants