diff --git a/private/buf/buftarget/controlling_workspace.go b/private/buf/buftarget/controlling_workspace.go index f2fe177cbf..4bc196a272 100644 --- a/private/buf/buftarget/controlling_workspace.go +++ b/private/buf/buftarget/controlling_workspace.go @@ -25,7 +25,7 @@ type ControllingWorkspace interface { // buf.work.yaml or v2 buf.yaml workspace configuration is located. Path() string // Returns a buf.work.yaml file that was found for the controlling workspace. - // This is empty if we are retruning a buf.yaml. + // This is empty if we are returning a buf.yaml. BufWorkYAMLFile() bufconfig.BufWorkYAMLFile // Returns a buf.yaml that was found for the controlling workspace. // This is empty if we are returning a buf.work.yaml. diff --git a/private/bufpkg/bufprotoplugin/bufprotoplugin.go b/private/bufpkg/bufprotoplugin/bufprotoplugin.go index 3b780e3e4b..1b07434031 100644 --- a/private/bufpkg/bufprotoplugin/bufprotoplugin.go +++ b/private/bufpkg/bufprotoplugin/bufprotoplugin.go @@ -108,7 +108,7 @@ type PluginResponse struct { PluginOut string } -// NewPluginResponse retruns a new *PluginResponse. +// NewPluginResponse returns a new *PluginResponse. func NewPluginResponse( response *pluginpb.CodeGeneratorResponse, pluginName string, diff --git a/private/pkg/protosourcepath/README.md b/private/pkg/protosourcepath/README.md index 8bf1af6a33..718828e7d1 100644 --- a/private/pkg/protosourcepath/README.md +++ b/private/pkg/protosourcepath/README.md @@ -1,186 +1,109 @@ # `protosourcepath` +`protosourcepath` is a simple package that takes a [Protobuf source path](source-path) and +returns a list of associated source paths. A [Protobuf source path](source-path) is a +[`SourceCodeInfo.Location`](location) path, which is an array of integers of a variable length +that identifies a Protobuf definition. Each element of the source path represents a field +number from a descriptor proto or an index, and they form a *path* from the [`FileDescriptorProto`](file-descriptor) +to the definition itself. An index is needed for any `repeated` types on the descriptor proto, +such as messages, enums, services, and extensions on [`FileDescriptorProto`](file-descriptor). + +For example, let's say we have the following source path: + +``` +[4, 0, 2, 0, 1] +``` + +This path represents `.message_type(0).field(0).name`, which is the name of field at index +0 for the message at index 0. We can break down the source path by following the numbers, +starting from `FileDescriptorProto`: + +- `4` is the field number of [`message_type` on `FileDescriptorProto`](message-types), which + is a repeated field representing message declarations in the file. `0` is the index of the + message for this path. +- `2` is the field number of [`field` on `DescriptorProto`](field), which is a repeated field + representing field declarations of a message. `0` is the index of the field for this path. +- `1` is the field number of [`name` on `FieldDescriptorProto`](field-name), which is a field + representing the name of field declarations. + +All source paths start from `FileDescriptorProto` and end at the Protobuf definition they +are pointing to. More details on source paths can be found in [descriptor.proto](source-path). + +Source paths are useful because they can be used to retrieve the [`SourceCodeInfo`](source-code-info) +of Protobuf definitions from their `FileDescriptorProto`'s. [`SourceCodeInfo`](source-code-info) provides +location-based metadata of Protobuf definitions, including comments attached to the Protobuf +definition, which we use to look for comment ignores for lint checks. And in the case of comment +ignores, a list of associated paths would allow us to check "associated locations" as potential +sites for defining a comment ignore (e.g. for a lint rule that checks fields, we want users to +be able to define a comment ignore on a specific field, but also the message a field belongs +to, since they could use that to ignore this rule for all fields for that message instead of +defining individual comment ignores for each field). + +## Associated paths + +Associated paths are source paths that we consider "associated" with the given source path, +which we define as parent paths or child paths. + +**Parent paths** are valid source paths to "complete" Protobuf declarations that are equal or "closer" +to the `FileDescriptorProto` than the given source path. A "complete" Protobuf declaration starts +from either the keyword (e.g. `message` or `enum`) or label/name (e.g. fields may or may not +have a label, and enum values would start at the name) and terminates at the opening brace +or semicolon respectively. For example, the path we looked at earlier, `[4, 0, 2, 0, 1]`, one of +the parent paths would be `[4, 0, 2, 0]`, which is the complete field declaration of `message_type(0).field(0)` +(which starts at the label of the field and terminates at the semicolon). Parent paths are +always "complete" Protobuf declarations. The following is a breakdown of what we consider as parent paths: + +- For each top-level declaration (e.g. messages, enums, services, extensions, options), we consider the + complete declaration as a parent path. This means that a given path can be one of its parent paths, + (e.g. if the given path is `[4, 0, 2, 0]`, then `[4, 0, 2, 0]` would be considered one of + the parent paths). +- For each nested declaration (e.g. field; enum values; options; nested messages, enums, and + extensions), we consider the complete declaration of the Protobuf definition as a parent path, + and the paths of the complete declarations of all parent types as parent paths (e.g. if the + given path is `[4, 0, 3, 2]`, which is a path to the complete declaration of a nested message, + then the path itself, `[4, 0, 3, 2]` is considered a parent path and `[4, 0]`, the path of the complete + declaration of the parent message is considered a parent path). +- For each specific attribute (e.g. name, label, field number, enum value number, etc.), we consider the complete + declaration of the Protobuf definition as a parent path (as illustrated in the initial example, + `[4, 0, 2, 0, 1]`, given the path to a field name, the complete declaration of the field + and the complete declaration of the message would be considered parent paths). + +**Child paths** are valid source paths that are *not* complete Protobuf declarations that are +equal or "closer" to the `FileDescriptorProto` than the given source path. Going back to our +example path, `[4, 0, 2, 0, 1]`, a path to a field name, it would be considered its own child +path, since it is not a complete Protobuf declaration, and other associated child paths would +include the field number, label, type, and type name. In addition, the associated child paths +of its parent type would also be considered associated child paths, in this case, the path +to the message name. + +Details examples for associated paths can be found through the tests. + ## API -- A single function, `GetAssociatedSourcePaths` that takes a `protoreflect.SourcePath` and - the option to `excludeChildAssociatedPaths`, and returns a list of associated paths, - `[]protoreflect.SourcePath`: +There is a single function, `GetAssociatedSourcePaths`, that takes a `protoreflect.SourcePath` +and returns a list of associated paths. ```go func GetAssociatedSourcePaths( sourcePath protoreflect.SourcePath, - excludeChildAssociatedPaths bool, ) ([]protoreflect.SourcePath, error) ``` -- We expect there always to be at least one associated path. -- `excludeChildAssociatedPaths` will exclude all child paths and parent associated child - paths (explained below). This is useful since comments are usually only included on - complete/top-level declarations. - - If the provided `sourcePath` is not a top-level declaration, e.g. a path to message - name `[4, 0, 1]`, this path will not be considered an associated path when `excludeChildAssociatedPaths` - is set to `true`. - -## Semantics - -- All associated paths are based on: - - Parent paths - - Child paths -- We do not consider all child paths as associated, only some. -- Some “child paths” may actually be the associated child paths of the parent path. -- For example, a path to a field definition, `[4, 0, 2, 0]` would have the following associated paths: - - `[4, 0, 2, 0]` the field definition - - `[4, 0]` the message definition (parent path) - - `[4, 0, 1]` the message name (parent associated child path) - - `[4, 0, 2, 0, 1]` the field name (child path) - - `[4, 0, 2, 0, 3]` the field number (child path) - - `[4, 0, 2, 0, 5]` the field type (child path) - - `[4, 0, 2, 0, 6]` the field type name (child path) - - `[4, 0, 2, 0, 4]` the field label (child path) -- If `excludeChildAssociatedPaths` is set to `true` , then child paths and parent-associated child - paths would not be included. So for the example above, we could get: - - `[4, 0, 2, 0]` the field definition - - `[4, 0]` the message definition (parent path) -- This does not do a validation on associated paths with existing spans, e.g. the example above, - there may not be a field label or field type name (such as the field declaration below): - -```protobuf -message Foo { - string id = 1; -} -``` +We expect there always to be at least one associated path, the path itself. -- This library/API does not care/know about spans — a path may have more than one span, e.g. - -```protobuf -message Bar { - extend Foo { - repeated int32 bar = 5; - optional string barbar = 6; - } - extend Foo { - repeated int32 barbarbar = 7; - } -} -``` +## Future + +We are currently returning all associated source paths, but we have the option to exclude +child paths/Protobuf declarations that are not complete, since our use-case is primarily to +get comments, which should only be attached to complete Protobuf declarations. However, it +is currently inexpensive to check all associated source paths, so we have not exposed that +functionality on the exported function. -- The path `[4, 0, 6]` would both have two spans, one with starting line 2, and the other - with starting line 6. This library does not parse span information, and it is up to the - caller to handle multiple spans. -- The library does not differentiate whether a field is a part of a `oneof` declaration — - it is up to the caller to check `FieldDescriptorProto.oneof_index` and then check the - associated paths for the `oneof_decl`. -- For options, the library will return the path itself and any associated parent paths — - even if it is not a known option from `descriptor.proto`. It only guarantees that an option - number is provided. -- For `default_value`, which is specific to `proto2`, we do not return it as an associated - path of a field, but the library will return associated paths when provided with `default_value`. - -### Associations and Errors - -A table of declarations and their associated paths are available below. When `excludeChildAssociatedPaths` -is set to `true`, then everything in the `child_associated_paths` column is excluded. The -`parent_associated_paths` column may include the path itself. - -| | | associated_paths | | | -|-----------------------------------------------------------|----------------------------------|----------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------|--------------------------------------------------------| -| path | | parent_associated_paths | child_associated_paths | invalid_path_error | -| .package | [2] | [2] | | | -| .dependency | [3] | | | cannot have dependency declaration without index | -| .dependency[i] | [3,i] | [3,i] | | | -| .syntax | [12] | [12] | | | -| .edition | [14] | [14] | | | -| .message_type | [4] | | | cannot have message declaration without index | -| .message_type[i] | [4, i] | [4, i] | [4, i, 1] | | -| .message_type[i].name | [4, i, 1] | [4,i] | [4, i, 1] | | -| .message_type[i].field | [4, i, 2] | | | cannot have field declaration without index | -| .message_type[i].field[j] | [4, i, 2, j] | [4,i] [4, i, 2, j] | [4, i, 1] [4, i, 2, j, 1] [4, i, 2, j, 3] [4, i, 2, j, 4] [4, i, 2, j, 5] [4, i, 2, j, 6] | | -| .message_type[i].field[j].name | [4, i, 2, j, 1] | [4,i] [4, i, 2, j] | [4, i, 1] [4, i, 2, j, 1] [4, i, 2, j, 3] [4, i, 2, j, 4] [4, i, 2, j, 5] [4, i, 2, j, 6] | | -| .message_type[i].field[j].number | [4, i, 2, j, 3] | [4,i] [4, i, 2, j] | [4, i, 1] [4, i, 2, j, 1] [4, i, 2, j, 3] [4, i, 2, j, 4] [4, i, 2, j, 5] [4, i, 2, j, 6] | | -| .message_type[i].field[j].label | [4, i, 2, j, 4] | [4,i] [4, i, 2, j] | [4, i, 1] [4, i, 2, j, 1] [4, i, 2, j, 3] [4, i, 2, j, 4] [4, i, 2, j, 5] [4, i, 2, j, 6] | | -| .message_type[i].field[j].type | [4, i, 2, j, 5] | [4,i] [4, i, 2, j] | [4, i, 1] [4, i, 2, j, 1] [4, i, 2, j, 3] [4, i, 2, j, 4] [4, i, 2, j, 5] [4, i, 2, j, 6] | | -| .message_type[i].field[j].type_name | [4, i, 2, j, 6] | [4,i] [4, i, 2, j] | [4, i, 1] [4, i, 2, j, 1] [4, i, 2, j, 3] [4, i, 2, j, 4] [4, i, 2, j, 5] [4, i, 2, j, 6] | | -| .message_type[i].nested_type | [4, i, 3] | | | cannot have nested declaration without index | -| .message_type[i].nested_type[j] | [4, i, 3, j] | [4,i] [4, i, 3, j] | [4, i, 1] [4, i, 3, j, 1] | | -| .message_type[i].nested_type[j].name | [4, i, 3, j, 1] | [4,i] [4, i, 3, j] | [4, i, 1] [4, i, 3, j, 1] | | -| .message_type[i].enum_type | [4, i, 4] | | | cannot have nested enum declaration without index | -| .message_type[i].enum_type[j] | [4, i, 4, j] | [4,i] [4, i, 4, j] | [4, i, 1] [4, i, 4, j, 1] | | -| .message_type[i].enum_type[j].name | [4, i, 4, j, 1] | [4,i] [4, i, 4, j] | [4, i, 1] [4, i, 4, j, 1] | | -| .message_type[i].extension_range | [4, i, 5] | [4,i] [4, i, 5] | [4, i, 1] | | -| .message_type[i].extension_range[j] | [4, i, 5, j] | [4,i] [4, i, 5] [4, i, 5, j] | [4, i, 1] [4, i, 5, j, 1] [4, i, 5, j, 2] | | -| .message_type[i].extension_range[j].start | [4, i, 5, j, 1] | [4,i] [4, i, 5] [4, i, 5, j] | [4, i, 1] [4, i, 5, j, 1] [4, i, 5, j, 2] | | -| .message_type[i].extension_range[j].end | [4, i, 5, j, 2] | [4,i] [4, i, 5] [4, i, 5, j] | [4, i, 1] [4, i, 5, j, 1] [4, i, 5, j, 2] | | -| .message_type[i].oneof_decl | [4, i, 8] | | | cannot have oneof declaration without index | -| .message_type[i].oneof_decl[j] | [4, i, 8, j] | [4,i] [4, i, 8, j] | [4, i, 1] [4, i, 8, j, 1] | | -| .message_type[i].oneof_decl[j].name | [4, i, 8, j, 1] | [4,i] [4, i, 8, j] | [4, i, 1] [4, i, 8, j, 1] | | -| .message_type[i].extension | [4, i, 6] | [4,i] [4, i, 6] | [4, i, 1] | | -| .message_type[i].extension[j] | [4, i, 6, j] | [4,i] [4, i, 6] [4, i, 6, j] | [4, i, 6, j, 2] [4, i, 6, j, 1] [4, i, 6, j, 3] [4, i, 6, j, 4] [4, i, 6, j, 5] [4, i, 6, j, 6] | | -| .message_type[i].extension[j].extendee | [4, i, 6, j, 2] | [4,i] [4, i, 6] [4, i, 6, j] | [4, i, 6, j, 2] [4, i, 6, j, 1] [4, i, 6, j, 3] [4, i, 6, j, 4] [4, i, 6, j, 5] [4, i, 6, j, 6] | | -| .message_type[i].extension[j].name | [4, i, 6, j, 1] | [4,i] [4, i, 6] [4, i, 6, j] | [4, i, 6, j, 2] [4, i, 6, j, 1] [4, i, 6, j, 3] [4, i, 6, j, 4] [4, i, 6, j, 5] [4, i, 6, j, 6] | | -| .message_type[i].extension[j].number | [4, i, 6, j, 3] | [4,i] [4, i, 6] [4, i, 6, j] | [4, i, 6, j, 2] [4, i, 6, j, 1] [4, i, 6, j, 3] [4, i, 6, j, 4] [4, i, 6, j, 5] [4, i, 6, j, 6] | | -| .message_type[i].extension[j].label | [4, i, 6, j, 4] | [4,i] [4, i, 6] [4, i, 6, j] | [4, i, 6, j, 2] [4, i, 6, j, 1] [4, i, 6, j, 3] [4, i, 6, j, 4] [4, i, 6, j, 5] [4, i, 6, j, 6] | | -| .message_type[i].extension[j].type | [4, i, 6, j, 5] | [4,i] [4, i, 6] [4, i, 6, j] | [4, i, 6, j, 2] [4, i, 6, j, 1] [4, i, 6, j, 3] [4, i, 6, j, 4] [4, i, 6, j, 5] [4, i, 6, j, 6] | | -| .message_type[i].extension[j].type_name | [4, i, 6, j, 6] | [4,i] [4, i, 6] [4, i, 6, j] | [4, i, 6, j, 2] [4, i, 6, j, 1] [4, i, 6, j, 3] [4, i, 6, j, 4] [4, i, 6, j, 5] [4, i, 6, j, 6] | | -| .message_type[i].reserved_range | [4, i, 9] | [4,i] [4, i, 9] | [4, i, 1] | | -| .message_type[i].reserved_range[j] | [4, i, 9, j] | [4,i] [4, i, 9] [4, i, 9, j] | [4, i, 1] [4, i, 9, j, 1] [4, i, 9, j, 2] | | -| .message_type[i].reserved_range[j].start | [4, i, 9, j, 1] | [4,i] [4, i, 9] [4, i, 9, j] | [4, i, 1] [4, i, 9, j, 1] [4, i, 9, j, 2] | | -| .message_type[i].reserved_range[j].end | [4, i, 9, j, 2] | [4,i] [4, i, 9] [4, i, 9, j] | [4, i, 1] [4, i, 9, j, 1] [4, i, 9, j, 2] | | -| .message[i].reserved_name | [4, i, 10] | [4, i] [4, i, 10] | [4, i, 1] | | -| .message[i].reserved_name[j] | [4, i, 10, j] | [4, i] [4, i, 10] [4, i, 10, j] | [4, i, 1] | | -| .enum_type | [5] | | | cannot have enum declaration without index | -| .enum_type[i] | [5, i] | [5, i] | [5, i, 1] | | -| .enum_type[i].name | [5, i, 1] | [5, i] | [5, i, 1] | | -| .enum_type[i].value | [5, i, 2] | | | cannot have enum value declaration without index | -| .enum_type[i].value[j] | [5, i, 2, j] | [5, i] [5, i, 2, j] | [5, i, 1] [5, i, 2, j, 1] [5, i, 2, j, 2] | | -| .enum_type[i].value[j].name | [5, i, 2, j, 1] | [5, i] [5, i, 2, j] | [5, i, 1] [5, i, 2, j, 1] [5, i, 2, j, 2] | | -| .enum_type[i].value[j].number | [5, i, 2, j, 2] | [5, i] [5, i, 2, j] | [5, i, 1] [5, i, 2, j, 1] [5, i, 2, j, 2] | | -| .enum_type[i].reserved_range | [5, i, 4] | [5, i] [5, i, 4] | [5, i, 1] | | -| .enum_type[i].reserved_range[j] | [5, i, 4, j] | [5, i] [5, i, 4] [5, i, 4, j] | [5, i, 1] [5, i, 4, j, 1] [5, i, 4, j, 2] | | -| .enum_type[i].reserved_range[j].start | [5, i, 4, j, 1] | [5, i] [5, i, 4] [5, i, 4, j] | [5, i, 1] [5, i, 4, j, 1] [5, i, 4, j, 2] | | -| .enum_type[i].reserved_range[j].end | [5, i, 4, j, 2] | [5, i] [5, i, 4] [5, i, 4, j] | [5, i, 1] [5, i, 4, j, 1] [5, i, 4, j, 2] | | -| .enum_type[i].reserved_name | [5, i, 5] | [5, i] [5, i, 5] | [5, i, 1] | | -| .enum_type[i].reserved_name[j] | [5, i, 5, j] | [5, i] [5, i, 5] [5, i, 5, j] | [5, i, 1] | | -| .extension | [7] | [7] | | | -| .extension[i] | [7, i] | [7] [7, i] | [7, i, 2] [7, i, 1] [7, i, 3] [7, i, 4] [7, i, 5] [7, i, 6] | | -| .extension[i].extendee | [7, i, 2] | [7] [7, i] | [7, i, 2] [7, i, 1] [7, i, 3] [7, i, 4] [7, i, 5] [7, i, 6] | | -| .extension[i].name | [7, i, 1] | [7] [7, i] | [7, i, 2] [7, i, 1] [7, i, 3] [7, i, 4] [7, i, 5] [7, i, 6] | | -| .extension[i].number | [7, i, 3] | [7] [7, i] | [7, i, 2] [7, i, 1] [7, i, 3] [7, i, 4] [7, i, 5] [7, i, 6] | | -| .extension[i].label | [7, i, 4] | [7] [7, i] | [7, i, 2] [7, i, 1] [7, i, 3] [7, i, 4] [7, i, 5] [7, i, 6] | | -| .extension[i].type | [7, i, 5] | [7] [7, i] | [7, i, 2] [7, i, 1] [7, i, 3] [7, i, 4] [7, i, 5] [7, i, 6] | | -| .extension[i].type_name | [7, i, 6] | [7] [7, i] | [7, i, 2] [7, i, 1] [7, i, 3] [7, i, 4] [7, i, 5] [7, i, 6] | | -| .service | [6] | | | cannot have service declaration without an index | -| .service[i] | [6, i] | [6, i] | [6, i, 1] | | -| .service[i].name | [6, i, 1] | [6, i] | [6, i, 1] | | -| .service[i].method | | | | cannot have method declaration without an index | -| .service[i].method[j] | [6, i, 2, j] | [6, i] [6, i, 2, j] | [6, i, 1] [6, i, 2, j, 1] [6, i, 2, j, 2] [6, i, 2, j, 3] [6, i, 2, j, 5] [6, i, 2, j, 6] | | -| .service[i].method[j].name | [6, i, 2, j, 1] | [6, i] [6, i, 2, j] | [6, i, 1] [6, i, 2, j, 1] [6, i, 2, j, 2] [6, i, 2, j, 3] [6, i, 2, j, 5] [6, i, 2, j, 6] | | -| .service[i].method[j].input_type | [6, i, 2, j, 2] | [6, i] [6, i, 2, j] | [6, i, 1] [6, i, 2, j, 1] [6, i, 2, j, 2] [6, i, 2, j, 3] [6, i, 2, j, 5] [6, i, 2, j, 6] | | -| .service[i].method[j].output_type | [6, i, 2, j, 3] | [6, i] [6, i, 2, j] | [6, i, 1] [6, i, 2, j, 1] [6, i, 2, j, 2] [6, i, 2, j, 3] [6, i, 2, j, 5] [6, i, 2, j, 6] | | -| .service[i].method[j].client_streaming | [6, i, 2, j, 5] | [6, i] [6, i, 2, j] | [6, i, 1] [6, i, 2, j, 1] [6, i, 2, j, 2] [6, i, 2, j, 3] [6, i, 2, j, 5] [6, i, 2, j, 6] | | -| .service[i].method[j].server_streaming | [6, i, 2, j, 6] | [6, i] [6, i, 2, j] | [6, i, 1] [6, i, 2, j, 1] [6, i, 2, j, 2] [6, i, 2, j, 3] [6, i, 2, j, 5] [6, i, 2, j, 6] | | -| .options | [8] | [8] | | | -| .options. | [8, ] | [8] [8, ] | | | -| .message_type[i].options | [4, i, 7] | [4, i] [4, i, 7] | [4, i, 1] | | -| .message_type[i].options. | [4, i, 7, ] | [4, i] [4, i, 7] [4, i, 7, ] | [4, i, 1] | | -| .message_type[i].nested_type[j].options | [4, i, 3, j, 7] | [4, i] [4, i, 3, j] | [4, i, 1] [4, i, 3, j, 1] | | -| .message_type[i].nested_type[j].options. | [4, i, 3, j, 7, ] | [4, i] [4, i, 3, j] [4, i, 3, j, 7, ] | [4, i, 1] [4, i, 3, j, 1] | | -| .message_type[i].enum_type[j].options | [4, i, 4, j, 3] | [4, i] [4, i, 4, j] | [4, i, 1] [4, i, 4, 1] | | -| .message_type[i].enum_type[j].options. | [4, i, 4, j, 3, ] | [4, i] [4, i, 4, j] [4, i, 4, j, 3, ] | [4, i, 1] [4, i, 4, 1] | | -| .message_type[i].field[j].options | [4, i, 2, j, 8] | [4, i] [4, i, 2, j] | [4, i, 1] [4, i, 2, j, 1] [4, i, 2, j, 3] [4, i, 2, j, 4] [4, i, 2, j, 5] [4, i, 2, j, 6] | | -| .message_type[i].field[j].options. | [4, i, 2, j, 8, ] | [4, i] [4, i, 2, j] [4, i, 2, j, 8, ] | [4, i, 1] [4, i, 2, j, 1] [4, i, 2, j, 3] [4, i, 2, j, 4] [4, i, 2, j, 5] [4, i, 2, j, 6] | | -| .mesasge_type[i].extension_range[j].options | [4, i, 5, j, 3] | [4,i] [4, i, 5] [4, i, 5, j] | [4, i, 1] [4, i, 5, j, 1] [4, i, 5, j, 2] | | -| .mesasge_type[i].extension_range[j].options. | [4, i, 5, j, 3, ] | [4,i] [4, i, 5] [4, i, 5, j] [4, i, 5, j, 3, ] | [4, i, 1] [4, i, 5, j, 1] [4, i, 5, j, 2] | | -| .message_type[i].oneof_decl[j].options | [4, i, 8, j, 2] | [4,i] [4, i, 8, j] | [4, i, 1] [4, i, 8, j, 1] | | -| .mesage_type[i].oneof_decl[j].options. | [4, i, 8, j, 2, ] | [4,i] [4, i, 8, j] [4, i, 8, j, 2, ] | [4, i, 1] [4, i, 8, j, 1] | | -| .message_type[i].extension[j].options | [4, i, 6, j, 8] | [4,i] [4, i, 6] [4, i, 6, j] | [4, i, 1] [4, i, 6, j, 2] [4, i, 6, j, 1] [4, i, 6, j, 3] [4, i, 6, j, 4] [4, i, 6, j, 5] [4, i, 6, j, 6] | | -| .message_type[i].extension[j].options. | [4, i, 6, j, 8, ] | [4,i] [4, i, 6] [4, i, 6, j] [4, i, 6, j, 8, ] | [4, i, 1] [4, i, 6, j, 2] [4, i, 6, j, 1] [4, i, 6, j, 3] [4, i, 6, j, 4] [4, i, 6, j, 5] [4, i, 6, j, 6] | | -| .extensions[i].options | [7, i, 8] | [7] [7, i] | [7, i, 2] [7, i, 1] [7, i, 3] [7, i, 4] [7, i, 5] [7, i, 6] | | -| .extensions[i].options. | [7, i, 8, ] | [7] [7, i] [7, i, 8, ] | [7, i, 2] [7, i, 1] [7, i, 3] [7, i, 4] [7, i, 5] [7, i, 6] | | -| .enum_type[i].options | [5, i, 3] | [5, i] [5, i, 3] | [5, i, 1] | | -| .enum_type[i].options. | [5, i, 3, ] | [5, i] [5, i, 3] [5, i, 3, ] | [5, i, 1] | | -| .enum_type[i].value[j].options | [5, i, 2, j, 3] | [5, i] [5, i, 2, j] [5, i, 2, j, 3] | [5, i, 1] [5, i, 2, j, 1] [5, i, 2, j, 2] | | -| .enum_type[i].value[j].options. | [5, i, 2, j, 3, ] | [5, i] [5, i, 2, j] [5, i, 2, j, 3] [5, i, 2, j, 3, ] | [5, i, 1] [5, i, 2, j, 1] [5, i, 2, j, 2] | | -| .service[i].options | [6, i, 3] | [6, i] [6, i, 3] | [6, i, 1] | | -| .service[i].options. | [6, i, 3, ] | [6, i] [6, i, 3] [6, i, 3, ] | [6, i, 1] | | -| .service[i].method[j].options | [6, i, 2, j, 4] | [6, i] [6, i, 2, j] [6, i, 2, j, 4] | [6, i, 1] [6, i, 2, j, 1] [6, i, 2, j, 2] [6, i, 2, j, 3] [6, i, 2, j, 5] | | -| .service[i].method[j].options. | [6, i, 2, j, 4, ] | [6, i] [6, i, 2, j] [6, i, 2, j, 4] [6, i, 2, j, 4, ] | [6, i, 1] [6, i, 2, j, 1] [6, i, 2, j, 2] [6, i, 2, j, 3] [6, i, 2, j, 5] | | +[location]: https://github.com/protocolbuffers/protobuf/blob/44e9777103aa864859c04159a7abc376c5a98210/src/google/protobuf/descriptor.proto#L1174 +[source-path]: https://github.com/protocolbuffers/protobuf/blob/44e9777103aa864859c04159a7abc376c5a98210/src/google/protobuf/descriptor.proto#L1175-L1197 +[file-descriptor]: https://github.com/protocolbuffers/protobuf/blob/44e9777103aa864859c04159a7abc376c5a98210/src/google/protobuf/descriptor.proto#L97 +[message-types]: https://github.com/protocolbuffers/protobuf/blob/44e9777103aa864859c04159a7abc376c5a98210/src/google/protobuf/descriptor.proto#L110 +[field]: https://github.com/protocolbuffers/protobuf/blob/44e9777103aa864859c04159a7abc376c5a98210/src/google/protobuf/descriptor.proto#L137 +[field-name]: https://github.com/protocolbuffers/protobuf/blob/44e9777103aa864859c04159a7abc376c5a98210/src/google/protobuf/descriptor.proto#L268 +[source-code-info]: https://github.com/protocolbuffers/protobuf/blob/44e9777103aa864859c04159a7abc376c5a98210/src/google/protobuf/descriptor.proto#L1129 +[dfa]: https://en.wikipedia.org/wiki/Deterministic_finite_automaton diff --git a/private/pkg/protosourcepath/enum.go b/private/pkg/protosourcepath/enum.go index 82fb5c5bc7..60d84d7c3b 100644 --- a/private/pkg/protosourcepath/enum.go +++ b/private/pkg/protosourcepath/enum.go @@ -27,22 +27,23 @@ const ( enumReservedNameTypeTag = int32(5) ) +// enums is the state when an element representing enums in the source path was parsed. func enums( _ int32, - sourcePath protoreflect.SourcePath, - i int, + fullSourcePath protoreflect.SourcePath, + index int, excludeChildAssociatedPaths bool, ) (state, []protoreflect.SourcePath, error) { associatedPaths := []protoreflect.SourcePath{ - currentPath(sourcePath, i), + currentPath(fullSourcePath, index), } if !excludeChildAssociatedPaths { associatedPaths = append( associatedPaths, - childAssociatedPath(sourcePath, i, enumNameTypeTag), + childAssociatedPath(fullSourcePath, index, enumNameTypeTag), ) } - if len(sourcePath) == i+1 { + if len(fullSourcePath) == index+1 { // This path does not extend beyond the enum declaration, return associated paths and // terminate here. return nil, associatedPaths, nil @@ -50,23 +51,32 @@ func enums( return enum, associatedPaths, nil } -func enum(token int32, sourcePath protoreflect.SourcePath, i int, _ bool) (state, []protoreflect.SourcePath, error) { +// enum is the state when an element representing a specific child path of an enum was parsed. +func enum(token int32, fullSourcePath protoreflect.SourcePath, index int, _ bool) (state, []protoreflect.SourcePath, error) { switch token { case enumNameTypeTag: // The enum name has already been added, can terminate here immediately. return nil, nil, nil case enumValuesTypeTag: - if len(sourcePath) < i+2 { - return nil, nil, newInvalidSourcePathError(sourcePath, "cannot have enum value declaration without index") + // We check to make sure that the length of the source path contains at least the current + // token and an index. This is because all source paths for enum values are expected + // to have indices. + if len(fullSourcePath) < index+2 { + return nil, nil, newInvalidSourcePathError(fullSourcePath, "cannot have enum value declaration without index") } return enumValues, nil, nil case enumOptionTypeTag: - // Return the entire path and then handle the option - return options, []protoreflect.SourcePath{slicesext.Copy(sourcePath)}, nil + // For options, we add the full path and then return the options state to validate + // the path. + return options, []protoreflect.SourcePath{slicesext.Copy(fullSourcePath)}, nil case enumReservedRangeTypeTag: - return reservedRanges, []protoreflect.SourcePath{currentPath(sourcePath, i)}, nil + // For reserved ranges, we add the full path and then return the reserved ranges state to + // validate the path. + return reservedRanges, []protoreflect.SourcePath{currentPath(fullSourcePath, index)}, nil case enumReservedNameTypeTag: - return reservedNames, []protoreflect.SourcePath{currentPath(sourcePath, i)}, nil + // For reserved names, we add the full path and then return the reserved names state to + // validate the path. + return reservedNames, []protoreflect.SourcePath{currentPath(fullSourcePath, index)}, nil } - return nil, nil, newInvalidSourcePathError(sourcePath, "invalid enum path") + return nil, nil, newInvalidSourcePathError(fullSourcePath, "invalid enum path") } diff --git a/private/pkg/protosourcepath/enumvalue.go b/private/pkg/protosourcepath/enumvalue.go index 6f3d5b9858..5b408b9493 100644 --- a/private/pkg/protosourcepath/enumvalue.go +++ b/private/pkg/protosourcepath/enumvalue.go @@ -32,23 +32,25 @@ var ( } ) +// enumValues is the state when an element representing enum values in the source path was +// parsed. func enumValues( _ int32, - sourcePath protoreflect.SourcePath, - i int, + fullSourcePath protoreflect.SourcePath, + index int, excludeChildAssociatedPaths bool, ) (state, []protoreflect.SourcePath, error) { associatedPaths := []protoreflect.SourcePath{ - currentPath(sourcePath, i), + currentPath(fullSourcePath, index), } if !excludeChildAssociatedPaths { associatedPaths = append( associatedPaths, - childAssociatedPath(sourcePath, i, enumValueNameTypeTag), - childAssociatedPath(sourcePath, i, enumValueNumberTypeTag), + childAssociatedPath(fullSourcePath, index, enumValueNameTypeTag), + childAssociatedPath(fullSourcePath, index, enumValueNumberTypeTag), ) } - if len(sourcePath) == i+1 { + if len(fullSourcePath) == index+1 { // This does not extend beyond the enum value declaration, return associated paths and // terminate here. return nil, associatedPaths, nil @@ -56,6 +58,8 @@ func enumValues( return enumValue, associatedPaths, nil } +// enumValue is the state when an element representing a specific child path of an enum was +// parsed. func enumValue(token int32, sourcePath protoreflect.SourcePath, i int, _ bool) (state, []protoreflect.SourcePath, error) { // TODO: use slices.Contains in the future if slicesext.ElementsContained( @@ -67,7 +71,8 @@ func enumValue(token int32, sourcePath protoreflect.SourcePath, i int, _ bool) ( } switch token { case enumValueOptionTypeTag: - // Return the entire path and then handle the option + // For options, we add the full path and then return the options state to validate + // the path. return options, []protoreflect.SourcePath{slicesext.Copy(sourcePath)}, nil } return nil, nil, newInvalidSourcePathError(sourcePath, "invalid enum value path") diff --git a/private/pkg/protosourcepath/field.go b/private/pkg/protosourcepath/field.go index ce758cbc42..8102ad4180 100644 --- a/private/pkg/protosourcepath/field.go +++ b/private/pkg/protosourcepath/field.go @@ -41,26 +41,27 @@ var ( } ) +// fields is the state when an element representing fields in the source path was parsed. func fields( _ int32, - sourcePath protoreflect.SourcePath, - i int, + fullSourcePath protoreflect.SourcePath, + index int, excludeChildAssociatedPaths bool, ) (state, []protoreflect.SourcePath, error) { associatedPaths := []protoreflect.SourcePath{ - currentPath(sourcePath, i), + currentPath(fullSourcePath, index), } if !excludeChildAssociatedPaths { associatedPaths = append( associatedPaths, - childAssociatedPath(sourcePath, i, fieldNameTypeTag), - childAssociatedPath(sourcePath, i, fieldNumberTypeTag), - childAssociatedPath(sourcePath, i, fieldLabelTypeTag), - childAssociatedPath(sourcePath, i, fieldTypeTypeTag), - childAssociatedPath(sourcePath, i, fieldTypeNameTypeTag), + childAssociatedPath(fullSourcePath, index, fieldNameTypeTag), + childAssociatedPath(fullSourcePath, index, fieldNumberTypeTag), + childAssociatedPath(fullSourcePath, index, fieldLabelTypeTag), + childAssociatedPath(fullSourcePath, index, fieldTypeTypeTag), + childAssociatedPath(fullSourcePath, index, fieldTypeNameTypeTag), ) } - if len(sourcePath) == i+1 { + if len(fullSourcePath) == index+1 { // This does not extend beyond the field declaration, return the associated paths and // terminate here. return nil, associatedPaths, nil @@ -68,7 +69,8 @@ func fields( return field, associatedPaths, nil } -func field(token int32, sourcePath protoreflect.SourcePath, i int, _ bool) (state, []protoreflect.SourcePath, error) { +// field is the state when an element representing a specific child path of a field was parsed. +func field(token int32, fullSourcePath protoreflect.SourcePath, index int, _ bool) (state, []protoreflect.SourcePath, error) { // TODO: use slices.Contains in the future if slicesext.ElementsContained( terminalFieldTokens, @@ -79,29 +81,34 @@ func field(token int32, sourcePath protoreflect.SourcePath, i int, _ bool) (stat } switch token { case fieldOptionTypeTag: - // Return the entire path and then handle the option - return options, []protoreflect.SourcePath{slicesext.Copy(sourcePath)}, nil + // For options, we add the full path and then return the options state to validate + // the path. + return options, []protoreflect.SourcePath{slicesext.Copy(fullSourcePath)}, nil case fieldDefaultValueTypeTag: - return nil, []protoreflect.SourcePath{currentPath(sourcePath, i)}, nil + // Default value is a terminal path, but was not already added to our associated paths, + // since default values are specific to proto2. Add the path and terminate. + return nil, []protoreflect.SourcePath{currentPath(fullSourcePath, index)}, nil } - return nil, nil, newInvalidSourcePathError(sourcePath, "invalid field path") + return nil, nil, newInvalidSourcePathError(fullSourcePath, "invalid field path") } +// extensions is the state when an element representing extensions in the source path was parsed. func extensions( token int32, - sourcePath protoreflect.SourcePath, - i int, + fullSourcePath protoreflect.SourcePath, + index int, excludeChildAssociatedPaths bool, ) (state, []protoreflect.SourcePath, error) { - // An extension is effectively a field descriptor, so we start by getting all paths for fields. - field, associatedPaths, err := fields(token, sourcePath, i, excludeChildAssociatedPaths) + // Extensions share the same descriptor proto definition as fields, so we can parse them + // using the same states. + field, associatedPaths, err := fields(token, fullSourcePath, index, excludeChildAssociatedPaths) if err != nil { return nil, nil, err } if !excludeChildAssociatedPaths { associatedPaths = append( associatedPaths, - childAssociatedPath(sourcePath, i, extensionExtendeeTypeTag), + childAssociatedPath(fullSourcePath, index, extensionExtendeeTypeTag), ) } return field, associatedPaths, nil diff --git a/private/pkg/protosourcepath/message.go b/private/pkg/protosourcepath/message.go index 294f61552b..6708686fdb 100644 --- a/private/pkg/protosourcepath/message.go +++ b/private/pkg/protosourcepath/message.go @@ -47,22 +47,23 @@ var ( } ) +// messages is the state when an element representing messages in the source path was parsed. func messages( _ int32, - sourcePath protoreflect.SourcePath, - i int, + fullSourcePath protoreflect.SourcePath, + index int, excludeChildAssociatedPaths bool, ) (state, []protoreflect.SourcePath, error) { associatedPaths := []protoreflect.SourcePath{ - currentPath(sourcePath, i), + currentPath(fullSourcePath, index), } if !excludeChildAssociatedPaths { associatedPaths = append( associatedPaths, - childAssociatedPath(sourcePath, i, messageNameTypeTag), + childAssociatedPath(fullSourcePath, index, messageNameTypeTag), ) } - if len(sourcePath) == i+1 { + if len(fullSourcePath) == index+1 { // This does not extend beyond the message declaration, return associated paths and // terminate here. return nil, associatedPaths, nil @@ -70,65 +71,89 @@ func messages( return message, associatedPaths, nil } -func message(token int32, sourcePath protoreflect.SourcePath, i int, _ bool) (state, []protoreflect.SourcePath, error) { +// message is the state when an element representing a specific child path of a message was parsed. +func message(token int32, fullSourcePath protoreflect.SourcePath, index int, _ bool) (state, []protoreflect.SourcePath, error) { switch token { case messageNameTypeTag: // The path for message name has already been added, can terminate here immediately. return nil, nil, nil case mesasgeFieldsTypeTag: - if len(sourcePath) < i+2 { - return nil, nil, newInvalidSourcePathError(sourcePath, "cannot have field declaration without index") + // We check to make sure that the length of the source path contains at least the current + // token and an index. This is because all source paths for fields are expected + // to have indices. + if len(fullSourcePath) < index+2 { + return nil, nil, newInvalidSourcePathError(fullSourcePath, "cannot have field declaration without index") } return fields, nil, nil case messageOneOfsTypeTag: - if len(sourcePath) < i+2 { - return nil, nil, newInvalidSourcePathError(sourcePath, "cannot have oneof declaration without index") + // We check to make sure that the length of the source path contains at least the current + // token and an index. This is because all source paths for oneofs are expected + // to have indices. + if len(fullSourcePath) < index+2 { + return nil, nil, newInvalidSourcePathError(fullSourcePath, "cannot have oneof declaration without index") } return oneOfs, nil, nil case nestedMessagesTypeTag: - if len(sourcePath) < i+2 { - return nil, nil, newInvalidSourcePathError(sourcePath, "cannot have a nested message declaration without index") + // We check to make sure that the length of the source path contains at least the current + // token and an index. This is because all source paths for nested messages are expected + // to have indices. + if len(fullSourcePath) < index+2 { + return nil, nil, newInvalidSourcePathError(fullSourcePath, "cannot have a nested message declaration without index") } return messages, nil, nil case nestedEnumsTypeTag: - if len(sourcePath) < i+2 { - return nil, nil, newInvalidSourcePathError(sourcePath, "cannot have a nested enum declaration without index") + // We check to make sure that the length of the source path contains at least the current + // token and an index. This is because all source paths for nested enums are expected + // to have indices. + if len(fullSourcePath) < index+2 { + return nil, nil, newInvalidSourcePathError(fullSourcePath, "cannot have a nested enum declaration without index") } return enums, nil, nil case messageOptionTypeTag: - // Return the entire path and then handle the option - return options, []protoreflect.SourcePath{slicesext.Copy(sourcePath)}, nil + // For options, we add the full path and then return the options state to validate + // the path. + return options, []protoreflect.SourcePath{slicesext.Copy(fullSourcePath)}, nil case messageExtensionRangeTypeTag: - return extensionRanges, []protoreflect.SourcePath{currentPath(sourcePath, i)}, nil + // For extension ranges, we add the full path and then return the extension ranges state + // to validate the path. + return extensionRanges, []protoreflect.SourcePath{currentPath(fullSourcePath, index)}, nil case messageExtensionsTypeTag: - return extensions, []protoreflect.SourcePath{currentPath(sourcePath, i)}, nil + // For extensions, we add the full path and then return the extensions state to + // validate the path. + return extensions, []protoreflect.SourcePath{currentPath(fullSourcePath, index)}, nil case messageReservedRangeTypeTag: - return reservedRanges, []protoreflect.SourcePath{currentPath(sourcePath, i)}, nil + // For reserved ranges, we add the full path and then return the reserved ranges state + // to validate the path. + return reservedRanges, []protoreflect.SourcePath{currentPath(fullSourcePath, index)}, nil case messageReservedNameTypeTag: - return reservedNames, []protoreflect.SourcePath{currentPath(sourcePath, i)}, nil + // For reserved names, we add the full path and then return the reserved names state to + // validate the path. + return reservedNames, []protoreflect.SourcePath{currentPath(fullSourcePath, index)}, nil } - return nil, nil, newInvalidSourcePathError(sourcePath, "invalid message path") + return nil, nil, newInvalidSourcePathError(fullSourcePath, "invalid message path") } +// oneOfs is the state when an element representing oneofs in the source path was parsed. func oneOfs( _ int32, - sourcePath protoreflect.SourcePath, - i int, + fullSourcePath protoreflect.SourcePath, + index int, excludeChildAssociatedPaths bool, ) (state, []protoreflect.SourcePath, error) { associatedPaths := []protoreflect.SourcePath{ - currentPath(sourcePath, i), + currentPath(fullSourcePath, index), } if !excludeChildAssociatedPaths { associatedPaths = append( associatedPaths, - childAssociatedPath(sourcePath, i, messageOneOfNameTypeTag), + childAssociatedPath(fullSourcePath, index, messageOneOfNameTypeTag), ) } return oneOf, associatedPaths, nil } -func oneOf(token int32, sourcePath protoreflect.SourcePath, i int, _ bool) (state, []protoreflect.SourcePath, error) { +// oneOf is the state when an element representing a specific child path of a oneof was parsed. +func oneOf(token int32, fullSourcePath protoreflect.SourcePath, _ int, _ bool) (state, []protoreflect.SourcePath, error) { // TODO: use slices.Contains in the future if slicesext.ElementsContained( terminalOneOfTokens, @@ -139,36 +164,40 @@ func oneOf(token int32, sourcePath protoreflect.SourcePath, i int, _ bool) (stat } switch token { case messageOneOfOptionTypeTag: - // Return the entire path and then handle the option - return options, []protoreflect.SourcePath{slicesext.Copy(sourcePath)}, nil + // For options, we add the full path and then return the options state to validate + // the path. + return options, []protoreflect.SourcePath{slicesext.Copy(fullSourcePath)}, nil } - return nil, nil, newInvalidSourcePathError(sourcePath, "invalid one of path") + return nil, nil, newInvalidSourcePathError(fullSourcePath, "invalid one of path") } +// extensionRanges is the state when an element representing extension ranges in the source path was parsed. func extensionRanges( _ int32, - sourcePath protoreflect.SourcePath, - i int, + fullSourcePath protoreflect.SourcePath, + index int, excludeChildAssociatedPaths bool, ) (state, []protoreflect.SourcePath, error) { associatedPaths := []protoreflect.SourcePath{ - currentPath(sourcePath, i), + currentPath(fullSourcePath, index), } if !excludeChildAssociatedPaths { associatedPaths = append( associatedPaths, - childAssociatedPath(sourcePath, i, messageExtensionRangeStartTypeTag), - childAssociatedPath(sourcePath, i, messageExtensionRangeEndTypeTag), + childAssociatedPath(fullSourcePath, index, messageExtensionRangeStartTypeTag), + childAssociatedPath(fullSourcePath, index, messageExtensionRangeEndTypeTag), ) } - if len(sourcePath) == i+1 { + if len(fullSourcePath) == index+1 { // This does not extend beyond the declaration, return associated paths and terminate here. return nil, associatedPaths, nil } return extensionRange, associatedPaths, nil } -func extensionRange(token int32, sourcePath protoreflect.SourcePath, i int, _ bool) (state, []protoreflect.SourcePath, error) { +// extensionRange is the state when an element representing a specific child path of an +// extension range was parsed. +func extensionRange(token int32, fullSourcePath protoreflect.SourcePath, _ int, _ bool) (state, []protoreflect.SourcePath, error) { // TODO: use slices.Contains in the future if slicesext.ElementsContained( terminalExtensionRangeTokens, @@ -179,8 +208,9 @@ func extensionRange(token int32, sourcePath protoreflect.SourcePath, i int, _ bo } switch token { case messageExtensionRangeOptionTypeTag: - // Return the entire path and then handle the option - return options, []protoreflect.SourcePath{slicesext.Copy(sourcePath)}, nil + // For options, we add the full path and then return the options state to validate + // the path. + return options, []protoreflect.SourcePath{slicesext.Copy(fullSourcePath)}, nil } - return nil, nil, newInvalidSourcePathError(sourcePath, "invalid extension range path") + return nil, nil, newInvalidSourcePathError(fullSourcePath, "invalid extension range path") } diff --git a/private/pkg/protosourcepath/method.go b/private/pkg/protosourcepath/method.go index 6beb7f8d4e..51afaf7f9c 100644 --- a/private/pkg/protosourcepath/method.go +++ b/private/pkg/protosourcepath/method.go @@ -38,26 +38,27 @@ var ( } ) +// methods is the state when an element representing methods in the source path was parsed. func methods( _ int32, - sourcePath protoreflect.SourcePath, - i int, + fullSourcePath protoreflect.SourcePath, + index int, excludeChildAssociatedPaths bool, ) (state, []protoreflect.SourcePath, error) { associatedPaths := []protoreflect.SourcePath{ - currentPath(sourcePath, i), + currentPath(fullSourcePath, index), } if !excludeChildAssociatedPaths { associatedPaths = append( associatedPaths, - childAssociatedPath(sourcePath, i, methodNameTypeTag), - childAssociatedPath(sourcePath, i, methodInputTypeTypeTag), - childAssociatedPath(sourcePath, i, methodOutputTypeTypeTag), - childAssociatedPath(sourcePath, i, methodClientStreamingTypeTag), - childAssociatedPath(sourcePath, i, methodServerStreamingTypeTag), + childAssociatedPath(fullSourcePath, index, methodNameTypeTag), + childAssociatedPath(fullSourcePath, index, methodInputTypeTypeTag), + childAssociatedPath(fullSourcePath, index, methodOutputTypeTypeTag), + childAssociatedPath(fullSourcePath, index, methodClientStreamingTypeTag), + childAssociatedPath(fullSourcePath, index, methodServerStreamingTypeTag), ) } - if len(sourcePath) == i+1 { + if len(fullSourcePath) == index+1 { // This does not extend beyond the method declaration, return associated paths and // terminate here. return nil, associatedPaths, nil @@ -65,7 +66,8 @@ func methods( return method, associatedPaths, nil } -func method(token int32, sourcePath protoreflect.SourcePath, i int, _ bool) (state, []protoreflect.SourcePath, error) { +// method is the state when an element representing a specific child path of a method was parsed. +func method(token int32, fullSourcePath protoreflect.SourcePath, _ int, _ bool) (state, []protoreflect.SourcePath, error) { // TODO: use slices.Contains in the future if slicesext.ElementsContained( terminalMethodTokens, @@ -76,8 +78,9 @@ func method(token int32, sourcePath protoreflect.SourcePath, i int, _ bool) (sta } switch token { case methodOptionTypeTag: - // Return the entire path and then handle the option - return options, []protoreflect.SourcePath{slicesext.Copy(sourcePath)}, nil + // For options, we add the full path and then return the options state to validate + // the path. + return options, []protoreflect.SourcePath{slicesext.Copy(fullSourcePath)}, nil } - return nil, nil, newInvalidSourcePathError(sourcePath, "invalid method path") + return nil, nil, newInvalidSourcePathError(fullSourcePath, "invalid method path") } diff --git a/private/pkg/protosourcepath/protosourcepath.go b/private/pkg/protosourcepath/protosourcepath.go index b4faff40b3..463bfe4995 100644 --- a/private/pkg/protosourcepath/protosourcepath.go +++ b/private/pkg/protosourcepath/protosourcepath.go @@ -43,39 +43,12 @@ var ( } ) -// GetAssociatedSourcePaths takes a protoreflect.SourcePath and the option to exclude child -// associated paths, and returns a list of associated paths, []protoreflect.SourcePath. +// GetAssociatedSourcePaths takes a protoreflect.SourcePath and returns a list of associated +// paths, []protoreflect.SourcePath. // // We should expect at least one associated path for a valid path input. // -// Excluding child associated paths will only return associated paths for complete/top-level -// declarations. For example, -// -// Input: [4, 0, 2, 0] (.message[0].field[0]) -// -// excludeChildAssociatedPaths == false: -// Associated paths: [ -// -// [4, 0] (.message[0]) -// [4, 0, 1] (.message[0].name) -// [4, 0, 2, 0] (.message[0].field[0]) -// [4, 0, 2, 0, 1] (.message[0].field[0].name) -// [4, 0, 2, 0, 3] (.message[0].field[0].number) -// [4, 0, 2, 0, 4] (.message[0].field[0].label) -// [4, 0, 2, 0, 5] (.message[0].field[0].type) -// [4, 0, 2, 0, 6] (.message[0].field[0].type_name) -// -// ] -// -// excludeChildAssociatedPaths == true: -// Associated paths: [ -// -// [4, 0] (.message[0]) -// [4, 0, 2, 0] (.message[0].field[0]) -// -// ] -// -// More details are available with the README for this package. +// More details on associated paths are available in the README.md. func GetAssociatedSourcePaths(sourcePath protoreflect.SourcePath) ([]protoreflect.SourcePath, error) { return getAssociatedSourcePaths(sourcePath, true) } @@ -90,13 +63,16 @@ func getAssociatedSourcePaths( var err error for i, token := range sourcePath { if currentState == nil { - // We returned an unexpected terminal state, this is considered an invalid source path. + // We have not parsed the entire source path, but received a terminal state, this is + // considered an invalid source path, return an error. return nil, newInvalidSourcePathError(sourcePath, "unexpected termination, invalid source path") } + // Check the currentState and then set the next state. currentState, associatedSourcePaths, err = currentState(token, sourcePath, i, excludeChildAssociatedPaths) if err != nil { return nil, err } + // Add all associated paths found to the result. if associatedSourcePaths != nil { result = append(result, associatedSourcePaths...) } @@ -107,96 +83,135 @@ func getAssociatedSourcePaths( // *** PRIVATE *** +// state represents a single state in a deterministic finite automaton (DFA). A DFA is used +// to parse the given source path. Each element of the source path (token) is checked with a +// state, and each state either returns the state to check the next element or it terminates +// the DFA. When the DFA is terminated, we do not expect to have additional elements that need +// to be parsed and all associated paths found are turned. type state func( + // token is the element of the source path that is currently being checked. token int32, - sourcePath protoreflect.SourcePath, + // fullSourcePath is the full source path being parsed. This is needed to construct associated + // source paths based on the token being checked. + fullSourcePath protoreflect.SourcePath, + // index is the index of the token that we are currently checking on the source path. index int, + // excludeChildAssociatedPaths, when set to true, will exclude child paths, which are not + // complete Protobuf declarations, from the associated source paths returned. excludeChildAssociatedPaths bool, ) (state, []protoreflect.SourcePath, error) -func start(token int32, sourcePath protoreflect.SourcePath, i int, _ bool) (state, []protoreflect.SourcePath, error) { +// start is the starting state and is used to parse the first element of the source path. +// It returns the subsequent state based on the token that was parsed. +func start(token int32, fullSourcePath protoreflect.SourcePath, index int, _ bool) (state, []protoreflect.SourcePath, error) { switch token { case packageTypeTag, syntaxTypeTag, editionTypeTag: // package, syntax, and edition are terminal paths, return the path and terminate here. - return nil, []protoreflect.SourcePath{currentPath(sourcePath, i)}, nil + return nil, []protoreflect.SourcePath{currentPath(fullSourcePath, index)}, nil case dependenciesTypeTag: - if len(sourcePath) < i+2 { - return nil, nil, newInvalidSourcePathError(sourcePath, "cannot have dependency declaration without index") + // We check to make sure that the length of the source path contains at least the current + // token and an index. This is because all source paths for dependencies are expected + // to have indices. + if len(fullSourcePath) < index+2 { + return nil, nil, newInvalidSourcePathError(fullSourcePath, "cannot have dependency declaration without index") } return dependencies, nil, nil case messagesTypeTag: - if len(sourcePath) < i+2 { - return nil, nil, newInvalidSourcePathError(sourcePath, "cannot have message declaration without index") + // We check to make sure that the length of the source path contains at least the current + // token and an index. This is because all source paths for messages are expected + // to have indices. + if len(fullSourcePath) < index+2 { + return nil, nil, newInvalidSourcePathError(fullSourcePath, "cannot have message declaration without index") } return messages, nil, nil case enumsTypeTag: - if len(sourcePath) < i+2 { - return nil, nil, newInvalidSourcePathError(sourcePath, "cannot have enum declaration without index") + // We check to make sure that the length of the source path contains at least the current + // token and an index. This is because all source paths for enums are expected + // to have indices. + if len(fullSourcePath) < index+2 { + return nil, nil, newInvalidSourcePathError(fullSourcePath, "cannot have enum declaration without index") } return enums, nil, nil case servicesTypeTag: - if len(sourcePath) < i+2 { - return nil, nil, newInvalidSourcePathError(sourcePath, "cannot have service declaration without index") + // We check to make sure that the length of the source path contains at least the current + // token and an index. This is because all source paths for services are expected + // to have indices. + if len(fullSourcePath) < index+2 { + return nil, nil, newInvalidSourcePathError(fullSourcePath, "cannot have service declaration without index") } return services, nil, nil case fileOptionsTypeTag: - // Return the entire path and then handle the option - return options, []protoreflect.SourcePath{slicesext.Copy(sourcePath)}, nil + // For options, we add the full path and then return the options state to validate + // the path. + return options, []protoreflect.SourcePath{slicesext.Copy(fullSourcePath)}, nil case extensionsTypeTag: - return extensions, []protoreflect.SourcePath{currentPath(sourcePath, i)}, nil + // For extensions, we add the full path and then return the extensions state to validate + // the path. + return extensions, []protoreflect.SourcePath{currentPath(fullSourcePath, index)}, nil } - return nil, nil, newInvalidSourcePathError(sourcePath, "invalid or unimplemented source path") + return nil, nil, newInvalidSourcePathError(fullSourcePath, "invalid source path") } -func dependencies(token int32, sourcePath protoreflect.SourcePath, i int, _ bool) (state, []protoreflect.SourcePath, error) { - // dependencies are a terminal path, retrun the path and terminate here. - return nil, []protoreflect.SourcePath{currentPath(sourcePath, i)}, nil +// dependencies is the state when an element representing dependencies in the source path +// was parsed. +func dependencies(token int32, sourcePath protoreflect.SourcePath, index int, _ bool) (state, []protoreflect.SourcePath, error) { + // Dependencies are considered a terminal path, we add the current path and then return. + return nil, []protoreflect.SourcePath{currentPath(sourcePath, index)}, nil } -func options(token int32, sourcePath protoreflect.SourcePath, i int, _ bool) (state, []protoreflect.SourcePath, error) { - // The entire path has alreaduy been returned, we just need to handle the terminal state here - if len(sourcePath) == i+1 { +// options is the state when an element representing options in the source path was parsed. +func options(token int32, fullSourcePath protoreflect.SourcePath, index int, _ bool) (state, []protoreflect.SourcePath, error) { + // We already added the full options path, this is considered a terminal state without + // additional information on the option for the source path. + if len(fullSourcePath) == index+1 { return nil, nil, nil } + // If there are additional path elements, we loop through them here. return options, nil, nil } +// reservedRanges is the state when an element representing reserved ranges in the source +// path was parsed. func reservedRanges( _ int32, - sourcePath protoreflect.SourcePath, - i int, + fullSourcePath protoreflect.SourcePath, + index int, excludeChildAssociatedPaths bool, ) (state, []protoreflect.SourcePath, error) { associatedPaths := []protoreflect.SourcePath{ - currentPath(sourcePath, i), + currentPath(fullSourcePath, index), } if !excludeChildAssociatedPaths { associatedPaths = append( associatedPaths, - childAssociatedPath(sourcePath, i, reservedRangeStartTypeTag), - childAssociatedPath(sourcePath, i, reservedRangeEndTypeTag), + childAssociatedPath(fullSourcePath, index, reservedRangeStartTypeTag), + childAssociatedPath(fullSourcePath, index, reservedRangeEndTypeTag), ) } return reservedRange, associatedPaths, nil } -func reservedRange(token int32, sourcePath protoreflect.SourcePath, i int, _ bool) (state, []protoreflect.SourcePath, error) { - // All reserved range paths are considered a terminal, so validate the path and terminate here. - // TODO: use slices.Contains in the future +// reservedRange is the state when an element representing a specific child path of a reserved +// range was parsed. +func reservedRange(token int32, fullSourcePath protoreflect.SourcePath, _ int, _ bool) (state, []protoreflect.SourcePath, error) { + // Reserved ranges are considered a terminal path, we validate the token to ensure that it + // is an expected element and return here. if !slicesext.ElementsContained( terminalReservedRangeTokens, []int32{token}, ) { - return nil, nil, newInvalidSourcePathError(sourcePath, "invalid reserved range path") + return nil, nil, newInvalidSourcePathError(fullSourcePath, "invalid reserved range path") } return nil, nil, nil } -func reservedNames(_ int32, sourcePath protoreflect.SourcePath, i int, _ bool) (state, []protoreflect.SourcePath, error) { +// reservedNames is the state when an element representing reserved names in the source +// path was parsed. +func reservedNames(_ int32, fullSourcePath protoreflect.SourcePath, index int, _ bool) (state, []protoreflect.SourcePath, error) { associatedPaths := []protoreflect.SourcePath{ - currentPath(sourcePath, i), + currentPath(fullSourcePath, index), } - // All reserved name paths are considered terminal, can terminate here immediately. + // Reserved names are considered a terminal path, we can terminal immediately. return nil, associatedPaths, nil } diff --git a/private/pkg/protosourcepath/service.go b/private/pkg/protosourcepath/service.go index 8950035970..5f0fa44bbc 100644 --- a/private/pkg/protosourcepath/service.go +++ b/private/pkg/protosourcepath/service.go @@ -25,22 +25,23 @@ const ( serviceOptionTypeTag = int32(3) ) +// services is the state when an element representing services in the source path was parsed. func services( _ int32, - sourcePath protoreflect.SourcePath, - i int, + fullSourcePath protoreflect.SourcePath, + index int, excludeChildAssociatedPaths bool, ) (state, []protoreflect.SourcePath, error) { associatedPaths := []protoreflect.SourcePath{ - currentPath(sourcePath, i), + currentPath(fullSourcePath, index), } if !excludeChildAssociatedPaths { associatedPaths = append( associatedPaths, - childAssociatedPath(sourcePath, i, serviceNameTypeTag), + childAssociatedPath(fullSourcePath, index, serviceNameTypeTag), ) } - if len(sourcePath) == +1 { + if len(fullSourcePath) == +1 { // This does not extend beyond the declaration, return associated paths and // terminate here. return nil, associatedPaths, nil @@ -48,19 +49,24 @@ func services( return service, associatedPaths, nil } -func service(token int32, sourcePath protoreflect.SourcePath, i int, _ bool) (state, []protoreflect.SourcePath, error) { +// service is the state when an element representing a specific child path of a service was parsed. +func service(token int32, fullSourcePath protoreflect.SourcePath, index int, _ bool) (state, []protoreflect.SourcePath, error) { switch token { case serviceNameTypeTag: // The path for service name has already been added, can termiante here immediately. return nil, nil, nil case serviceMethodsTypeTag: - if len(sourcePath) < i+2 { - return nil, nil, newInvalidSourcePathError(sourcePath, "cannot have method declaration without index") + // We check to make sure that the length of the source path contains at least the current + // token and an index. This is because all source paths for methods are expected + // to have indices. + if len(fullSourcePath) < index+2 { + return nil, nil, newInvalidSourcePathError(fullSourcePath, "cannot have method declaration without index") } return methods, nil, nil case serviceOptionTypeTag: - // Return the entire path and then handle the option - return options, []protoreflect.SourcePath{slicesext.Copy(sourcePath)}, nil + // For options, we add the full path and then return the options state to validate + // the path. + return options, []protoreflect.SourcePath{slicesext.Copy(fullSourcePath)}, nil } - return nil, nil, newInvalidSourcePathError(sourcePath, "invalid service path") + return nil, nil, newInvalidSourcePathError(fullSourcePath, "invalid service path") }