• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2020 The Pigweed Authors
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may not
4# use this file except in compliance with the License. You may obtain a copy of
5# the License at
6#
7#     https://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations under
13# the License.
14"""This module defines data structures for protobuf entities."""
15
16from __future__ import annotations
17
18import abc
19import collections
20import enum
21import itertools
22
23from typing import (
24    Callable,
25    Iterator,
26    TypeVar,
27    cast,
28)
29
30from google.protobuf import descriptor_pb2
31
32from pw_protobuf import options, symbol_name_mapping
33from pw_protobuf_codegen_protos.codegen_options_pb2 import CodegenOptions
34from pw_protobuf_protos.field_options_pb2 import pwpb as pwpb_field_options
35
36T = TypeVar('T')  # pylint: disable=invalid-name
37
38# Currently, protoc does not do a traversal to look up the package name of all
39# messages that are referenced in the file. For such "external" message names,
40# we are unable to find where the "::pwpb" subnamespace would be inserted by our
41# codegen. This namespace provides us with an alternative, more verbose
42# namespace that the codegen can use as a fallback in these cases. For example,
43# for the symbol name `my.external.package.ProtoMsg.SubMsg`, we would use
44# `::pw::pwpb_codegen_private::my::external::package:ProtoMsg::SubMsg` to refer
45# to the pw_protobuf generated code, when package name info is not available.
46#
47# TODO: b/258832150 - Explore removing this if possible
48EXTERNAL_SYMBOL_WORKAROUND_NAMESPACE = 'pw::pwpb_codegen_private'
49
50
51class ProtoNode(abc.ABC):
52    """A ProtoNode represents a C++ scope mapping of an entity in a .proto file.
53
54    Nodes form a tree beginning at a top-level (global) scope, descending into a
55    hierarchy of .proto packages and the messages and enums defined within them.
56    """
57
58    class Type(enum.Enum):
59        """The type of a ProtoNode.
60
61        PACKAGE maps to a C++ namespace.
62        MESSAGE maps to a C++ "Encoder" class within its own namespace.
63        ENUM maps to a C++ enum within its parent's namespace.
64        EXTERNAL represents a node defined within a different compilation unit.
65        SERVICE represents an RPC service definition.
66        """
67
68        PACKAGE = 1
69        MESSAGE = 2
70        ENUM = 3
71        EXTERNAL = 4
72        SERVICE = 5
73
74    def __init__(self, name: str):
75        self._name: str = name
76        self._children: dict[str, ProtoNode] = collections.OrderedDict()
77        self._parent: ProtoNode | None = None
78
79    @abc.abstractmethod
80    def type(self) -> ProtoNode.Type:
81        """The type of the node."""
82
83    def children(self) -> list[ProtoNode]:
84        return list(self._children.values())
85
86    def parent(self) -> ProtoNode | None:
87        return self._parent
88
89    def name(self) -> str:
90        return self._name
91
92    def cpp_name(self) -> str:
93        """The name of this node in generated C++ code."""
94        return symbol_name_mapping.fix_cc_identifier(self._name).replace(
95            '.', '::'
96        )
97
98    def _package_or_external(self) -> ProtoNode:
99        """Returns this node's deepest package or external ancestor node.
100
101        This method may need to return an external node, as a fallback for
102        external names that are referenced, but not processed into a more
103        regular proto tree. This is because there is no way to find the package
104        name of a node referring to an external symbol.
105        """
106        node: ProtoNode | None = self
107        while (
108            node
109            and node.type() != ProtoNode.Type.PACKAGE
110            and node.type() != ProtoNode.Type.EXTERNAL
111        ):
112            node = node.parent()
113
114        assert node, 'proto tree was built without a root'
115        return node
116
117    def cpp_namespace(
118        self,
119        root: ProtoNode | None = None,
120        codegen_subnamespace: str | None = 'pwpb',
121    ) -> str:
122        """C++ namespace of the node, up to the specified root.
123
124        Args:
125          root: Namespace from which this ProtoNode is referred. If this
126            ProtoNode has `root` as an ancestor namespace, then the ancestor
127            namespace scopes above `root` are omitted.
128
129          codegen_subnamespace: A subnamespace that is appended to the package
130            declared in the .proto file. It is appended to the declared package,
131            but before any namespaces that are needed for messages etc. This
132            feature can be used to allow different codegen tools to output
133            different, non-conflicting symbols for the same protos.
134
135            By default, this is "pwpb", which reflects the default behaviour
136            of the pwpb codegen.
137        """
138        self_pkg_or_ext = self._package_or_external()
139        root_pkg_or_ext = (
140            root._package_or_external()  # pylint: disable=protected-access
141            if root is not None
142            else None
143        )
144        if root_pkg_or_ext:
145            assert root_pkg_or_ext.type() != ProtoNode.Type.EXTERNAL
146
147        def compute_hierarchy() -> Iterator[str]:
148            same_package = True
149
150            if self_pkg_or_ext.type() == ProtoNode.Type.EXTERNAL:
151                # Can't figure out where the namespace cutoff is. Punt to using
152                # the external symbol workaround.
153                #
154                # TODO: b/250945489 - Investigate removing this limitation /
155                # hack
156                return itertools.chain(
157                    [EXTERNAL_SYMBOL_WORKAROUND_NAMESPACE],
158                    self._attr_hierarchy(ProtoNode.cpp_name, root=None),
159                )
160
161            if root is None or root_pkg_or_ext is None:  # extra check for mypy
162                # TODO: b/250945489 - maybe elide "::{codegen_subnamespace}"
163                # here, if this node doesn't have any package?
164                same_package = False
165            else:
166                paired_hierarchy = itertools.zip_longest(
167                    self_pkg_or_ext._attr_hierarchy(  # pylint: disable=protected-access
168                        ProtoNode.cpp_name, root=None
169                    ),
170                    root_pkg_or_ext._attr_hierarchy(  # pylint: disable=protected-access
171                        ProtoNode.cpp_name, root=None
172                    ),
173                )
174                for str_a, str_b in paired_hierarchy:
175                    if str_a != str_b:
176                        same_package = False
177                        break
178
179            if same_package:
180                # This ProtoNode and the requested root are in the same package,
181                # so the `codegen_subnamespace` should be omitted.
182                hierarchy = self._attr_hierarchy(ProtoNode.cpp_name, root)
183                return hierarchy
184
185            # The given root is either effectively nonexistent (common ancestor
186            # is ""), or is only a partial match for the package of this node.
187            # Either way, we will have to insert `codegen_subnamespace` after
188            # the relevant package string.
189            package_hierarchy = self_pkg_or_ext._attr_hierarchy(  # pylint: disable=protected-access
190                ProtoNode.cpp_name, root
191            )
192            maybe_subnamespace = (
193                [codegen_subnamespace] if codegen_subnamespace else []
194            )
195            inside_hierarchy = self._attr_hierarchy(
196                ProtoNode.cpp_name, self_pkg_or_ext
197            )
198
199            hierarchy = itertools.chain(
200                package_hierarchy, maybe_subnamespace, inside_hierarchy
201            )
202            return hierarchy
203
204        joined_namespace = '::'.join(
205            name for name in compute_hierarchy() if name
206        )
207
208        return (
209            '' if joined_namespace == codegen_subnamespace else joined_namespace
210        )
211
212    def proto_path(self) -> str:
213        """Fully-qualified package path of the node."""
214        path = '.'.join(self._attr_hierarchy(lambda node: node.name(), None))
215        return path.lstrip('.')
216
217    def pwpb_struct(self) -> str:
218        """Name of the pw_protobuf struct for this proto."""
219        return '::' + self.cpp_namespace() + '::Message'
220
221    def pwpb_table(self) -> str:
222        """Name of the pw_protobuf table constant for this proto."""
223        return '::' + self.cpp_namespace() + '::kMessageFields'
224
225    def nanopb_fields(self) -> str:
226        """Name of the Nanopb variable that represents the proto fields."""
227        return self._nanopb_name() + '_fields'
228
229    def nanopb_struct(self) -> str:
230        """Name of the Nanopb struct for this proto."""
231        return '::' + self._nanopb_name()
232
233    def _nanopb_name(self) -> str:
234        name = '_'.join(self._attr_hierarchy(lambda node: node.name(), None))
235        return name.lstrip('_')
236
237    def common_ancestor(self, other: ProtoNode) -> ProtoNode | None:
238        """Finds the earliest common ancestor of this node and other."""
239
240        if other is None:
241            return None
242
243        own_depth = self.depth()
244        other_depth = other.depth()
245        diff = abs(own_depth - other_depth)
246
247        if own_depth < other_depth:
248            first: ProtoNode | None = self
249            second: ProtoNode | None = other
250        else:
251            first = other
252            second = self
253
254        while diff > 0:
255            assert second is not None
256            second = second.parent()
257            diff -= 1
258
259        while first != second:
260            if first is None or second is None:
261                return None
262
263            first = first.parent()
264            second = second.parent()
265
266        return first
267
268    def depth(self) -> int:
269        """Returns the depth of this node from the root."""
270        depth = 0
271        node = self._parent
272        while node:
273            depth += 1
274            node = node.parent()
275        return depth
276
277    def add_child(self, child: ProtoNode) -> None:
278        """Inserts a new node into the tree as a child of this node.
279
280        Args:
281          child: The node to insert.
282
283        Raises:
284          ValueError: This node does not allow nesting the given type of child.
285        """
286        if not self._supports_child(child):
287            raise ValueError(
288                'Invalid child %s for node of type %s'
289                % (child.type(), self.type())
290            )
291
292        # pylint: disable=protected-access
293        if child._parent is not None:
294            del child._parent._children[child.name()]
295
296        child._parent = self
297        self._children[child.name()] = child
298        # pylint: enable=protected-access
299
300    def find(self, path: str) -> ProtoNode | None:
301        """Finds a node within this node's subtree.
302
303        Args:
304          path: The path to the sought node.
305        """
306        node = self
307
308        # pylint: disable=protected-access
309        for section in path.split('.'):
310            child = node._children.get(section)
311            if child is None:
312                return None
313            node = child
314        # pylint: enable=protected-access
315
316        return node
317
318    def __iter__(self) -> Iterator[ProtoNode]:
319        """Iterates depth-first through all nodes in this node's subtree."""
320        yield self
321        for child_iterator in self._children.values():
322            for child in child_iterator:
323                yield child
324
325    def _attr_hierarchy(
326        self,
327        attr_accessor: Callable[[ProtoNode], T],
328        root: ProtoNode | None,
329    ) -> Iterator[T]:
330        """Fetches node attributes at each level of the tree from the root.
331
332        Args:
333          attr_accessor: Function which extracts attributes from a ProtoNode.
334          root: The node at which to terminate.
335
336        Returns:
337          An iterator to a list of the selected attributes from the root to the
338          current node.
339        """
340        hierarchy = []
341        node: ProtoNode | None = self
342        while node is not None and node != root:
343            hierarchy.append(attr_accessor(node))
344            node = node.parent()
345        return reversed(hierarchy)
346
347    @abc.abstractmethod
348    def _supports_child(self, child: ProtoNode) -> bool:
349        """Returns True if child is a valid child type for the current node."""
350
351
352class ProtoPackage(ProtoNode):
353    """A protobuf package."""
354
355    def type(self) -> ProtoNode.Type:
356        return ProtoNode.Type.PACKAGE
357
358    def _supports_child(self, child: ProtoNode) -> bool:
359        return True
360
361
362class ProtoEnum(ProtoNode):
363    """Representation of an enum in a .proto file."""
364
365    def __init__(self, name: str):
366        super().__init__(name)
367        self._values: list[tuple[str, int]] = []
368
369    def type(self) -> ProtoNode.Type:
370        return ProtoNode.Type.ENUM
371
372    def values(self) -> list[tuple[str, int]]:
373        return list(self._values)
374
375    def add_value(self, name: str, value: int) -> None:
376        self._values.append(
377            (
378                ProtoMessageField.upper_snake_case(
379                    symbol_name_mapping.fix_cc_enum_value_name(name)
380                ),
381                value,
382            )
383        )
384
385    def _supports_child(self, child: ProtoNode) -> bool:
386        # Enums cannot have nested children.
387        return False
388
389
390class ProtoMessage(ProtoNode):
391    """Representation of a message in a .proto file."""
392
393    def __init__(self, name: str):
394        super().__init__(name)
395        self._fields: list[ProtoMessageField] = []
396        self._dependencies: list[ProtoMessage] | None = None
397        self._dependency_cycles: list[ProtoMessage] = []
398
399    def type(self) -> ProtoNode.Type:
400        return ProtoNode.Type.MESSAGE
401
402    def fields(self) -> list[ProtoMessageField]:
403        return list(self._fields)
404
405    def add_field(self, field: ProtoMessageField) -> None:
406        self._fields.append(field)
407
408    def _supports_child(self, child: ProtoNode) -> bool:
409        return (
410            child.type() == self.Type.ENUM or child.type() == self.Type.MESSAGE
411        )
412
413    def dependencies(self) -> list[ProtoMessage]:
414        if self._dependencies is None:
415            self._dependencies = []
416            for field in self._fields:
417                if (
418                    field.type()
419                    != descriptor_pb2.FieldDescriptorProto.TYPE_MESSAGE
420                ):
421                    continue
422
423                type_node = field.type_node()
424                assert type_node is not None
425                if type_node.type() == ProtoNode.Type.MESSAGE:
426                    self._dependencies.append(cast(ProtoMessage, type_node))
427
428        return list(self._dependencies)
429
430    def dependency_cycles(self) -> list[ProtoMessage]:
431        return list(self._dependency_cycles)
432
433    def remove_dependency_cycle(self, dependency: ProtoMessage):
434        assert self._dependencies is not None
435        assert dependency in self._dependencies
436        self._dependencies.remove(dependency)
437        self._dependency_cycles.append(dependency)
438
439
440class ProtoService(ProtoNode):
441    """Representation of a service in a .proto file."""
442
443    def __init__(self, name: str):
444        super().__init__(name)
445        self._methods: list[ProtoServiceMethod] = []
446
447    def type(self) -> ProtoNode.Type:
448        return ProtoNode.Type.SERVICE
449
450    def methods(self) -> list[ProtoServiceMethod]:
451        return list(self._methods)
452
453    def add_method(self, method: ProtoServiceMethod) -> None:
454        self._methods.append(method)
455
456    def _supports_child(self, child: ProtoNode) -> bool:
457        return False
458
459
460class ProtoExternal(ProtoNode):
461    """A node from a different compilation unit.
462
463    An external node is one that isn't defined within the current compilation
464    unit, most likely as it comes from an imported proto file. Its type is not
465    known, so it does not have any members or additional data. Its purpose
466    within the node graph is to provide namespace resolution between compile
467    units.
468    """
469
470    def type(self) -> ProtoNode.Type:
471        return ProtoNode.Type.EXTERNAL
472
473    def _supports_child(self, child: ProtoNode) -> bool:
474        return True
475
476
477# This class is not a node and does not appear in the proto tree.
478# Fields belong to proto messages and are processed separately.
479class ProtoMessageField:
480    """Representation of a field within a protobuf message."""
481
482    def __init__(
483        self,
484        field_name: str,
485        field_number: int,
486        field_type: int,
487        type_node: ProtoNode | None = None,
488        optional: bool = False,
489        repeated: bool = False,
490        codegen_options: CodegenOptions | None = None,
491    ):
492        self._field_name = symbol_name_mapping.fix_cc_identifier(field_name)
493        self._number: int = field_number
494        self._type: int = field_type
495        self._type_node: ProtoNode | None = type_node
496        self._optional: bool = optional
497        self._repeated: bool = repeated
498        self._options: CodegenOptions | None = codegen_options
499
500    def name(self) -> str:
501        return self.upper_camel_case(self._field_name)
502
503    def field_name(self) -> str:
504        return self._field_name
505
506    def enum_name(self) -> str:
507        return 'k' + self.name()
508
509    def legacy_enum_name(self) -> str:
510        return self.upper_snake_case(
511            symbol_name_mapping.fix_cc_enum_value_name(self._field_name)
512        )
513
514    def number(self) -> int:
515        return self._number
516
517    def type(self) -> int:
518        return self._type
519
520    def type_node(self) -> ProtoNode | None:
521        return self._type_node
522
523    def is_optional(self) -> bool:
524        return self._optional
525
526    def is_repeated(self) -> bool:
527        return self._repeated
528
529    def options(self) -> CodegenOptions | None:
530        return self._options
531
532    @staticmethod
533    def upper_camel_case(field_name: str) -> str:
534        """Converts a field name to UpperCamelCase."""
535        name_components = field_name.split('_')
536        return ''.join([word.lower().capitalize() for word in name_components])
537
538    @staticmethod
539    def upper_snake_case(field_name: str) -> str:
540        """Converts a field name to UPPER_SNAKE_CASE."""
541        return field_name.upper()
542
543
544class ProtoServiceMethod:
545    """A method defined in a protobuf service."""
546
547    class Type(enum.Enum):
548        UNARY = 'kUnary'
549        SERVER_STREAMING = 'kServerStreaming'
550        CLIENT_STREAMING = 'kClientStreaming'
551        BIDIRECTIONAL_STREAMING = 'kBidirectionalStreaming'
552
553        def cc_enum(self) -> str:
554            """Returns the pw_rpc MethodType C++ enum for this method type."""
555            return '::pw::rpc::MethodType::' + self.value
556
557    def __init__(
558        self,
559        service: ProtoService,
560        name: str,
561        method_type: Type,
562        request_type: ProtoNode,
563        response_type: ProtoNode,
564    ):
565        self._service = service
566        self._name = name
567        self._type = method_type
568        self._request_type = request_type
569        self._response_type = response_type
570
571    def service(self) -> ProtoService:
572        return self._service
573
574    def name(self) -> str:
575        return self._name
576
577    def type(self) -> Type:
578        return self._type
579
580    def server_streaming(self) -> bool:
581        return self._type in (
582            self.Type.SERVER_STREAMING,
583            self.Type.BIDIRECTIONAL_STREAMING,
584        )
585
586    def client_streaming(self) -> bool:
587        return self._type in (
588            self.Type.CLIENT_STREAMING,
589            self.Type.BIDIRECTIONAL_STREAMING,
590        )
591
592    def request_type(self) -> ProtoNode:
593        return self._request_type
594
595    def response_type(self) -> ProtoNode:
596        return self._response_type
597
598
599def _add_enum_fields(enum_node: ProtoNode, proto_enum) -> None:
600    """Adds fields from a protobuf enum descriptor to an enum node."""
601    assert enum_node.type() == ProtoNode.Type.ENUM
602    enum_node = cast(ProtoEnum, enum_node)
603
604    for value in proto_enum.value:
605        enum_node.add_value(value.name, value.number)
606
607
608def _create_external_nodes(root: ProtoNode, path: str) -> ProtoNode:
609    """Creates external nodes for a path starting from the given root."""
610
611    node = root
612    for part in path.split('.'):
613        child = node.find(part)
614        if not child:
615            child = ProtoExternal(part)
616            node.add_child(child)
617        node = child
618
619    return node
620
621
622def _find_or_create_node(
623    global_root: ProtoNode, package_root: ProtoNode, path: str
624) -> ProtoNode:
625    """Searches the proto tree for a node by path, creating it if not found."""
626
627    if path[0] == '.':
628        # Fully qualified path.
629        root_relative_path = path[1:]
630        search_root = global_root
631    else:
632        root_relative_path = path
633        search_root = package_root
634
635    node = search_root.find(root_relative_path)
636    if node is None:
637        # Create nodes for field types that don't exist within this
638        # compilation context, such as those imported from other .proto
639        # files.
640        node = _create_external_nodes(search_root, root_relative_path)
641
642    return node
643
644
645def _add_message_fields(
646    global_root: ProtoNode,
647    package_root: ProtoNode,
648    message: ProtoNode,
649    proto_message,
650    proto_options,
651) -> None:
652    """Adds fields from a protobuf message descriptor to a message node."""
653    assert message.type() == ProtoNode.Type.MESSAGE
654    message = cast(ProtoMessage, message)
655
656    type_node: ProtoNode | None
657
658    for field in proto_message.field:
659        if field.type_name:
660            # The "type_name" member contains the global .proto path of the
661            # field's type object, for example ".pw.protobuf.test.KeyValuePair".
662            # Try to find the node for this object within the current context.
663            type_node = _find_or_create_node(
664                global_root, package_root, field.type_name
665            )
666        else:
667            type_node = None
668
669        optional = field.proto3_optional
670        repeated = (
671            field.label == descriptor_pb2.FieldDescriptorProto.LABEL_REPEATED
672        )
673
674        codegen_options = (
675            options.match_options(
676                '.'.join((message.proto_path(), field.name)), proto_options
677            )
678            if proto_options is not None
679            else None
680        )
681
682        field_options = (
683            options.create_from_field_options(
684                field.options.Extensions[pwpb_field_options]
685            )
686            if field.options.HasExtension(pwpb_field_options)
687            else None
688        )
689
690        merged_options = None
691
692        if field_options and codegen_options:
693            merged_options = options.merge_field_and_codegen_options(
694                field_options, codegen_options
695            )
696        elif field_options:
697            merged_options = field_options
698        elif codegen_options:
699            merged_options = codegen_options
700
701        message.add_field(
702            ProtoMessageField(
703                field.name,
704                field.number,
705                field.type,
706                type_node,
707                optional,
708                repeated,
709                merged_options,
710            )
711        )
712
713
714def _add_service_methods(
715    global_root: ProtoNode,
716    package_root: ProtoNode,
717    service: ProtoNode,
718    proto_service,
719) -> None:
720    assert service.type() == ProtoNode.Type.SERVICE
721    service = cast(ProtoService, service)
722
723    for method in proto_service.method:
724        if method.client_streaming and method.server_streaming:
725            method_type = ProtoServiceMethod.Type.BIDIRECTIONAL_STREAMING
726        elif method.client_streaming:
727            method_type = ProtoServiceMethod.Type.CLIENT_STREAMING
728        elif method.server_streaming:
729            method_type = ProtoServiceMethod.Type.SERVER_STREAMING
730        else:
731            method_type = ProtoServiceMethod.Type.UNARY
732
733        request_node = _find_or_create_node(
734            global_root, package_root, method.input_type
735        )
736        response_node = _find_or_create_node(
737            global_root, package_root, method.output_type
738        )
739
740        service.add_method(
741            ProtoServiceMethod(
742                service, method.name, method_type, request_node, response_node
743            )
744        )
745
746
747def _populate_fields(
748    proto_file: descriptor_pb2.FileDescriptorProto,
749    global_root: ProtoNode,
750    package_root: ProtoNode,
751    proto_options: options.ParsedOptions | None,
752) -> None:
753    """Traverses a proto file, adding all message and enum fields to a tree."""
754
755    def populate_message(node, message):
756        """Recursively populates nested messages and enums."""
757        _add_message_fields(
758            global_root, package_root, node, message, proto_options
759        )
760
761        for proto_enum in message.enum_type:
762            _add_enum_fields(node.find(proto_enum.name), proto_enum)
763        for msg in message.nested_type:
764            populate_message(node.find(msg.name), msg)
765
766    # Iterate through the proto file, populating top-level objects.
767    for proto_enum in proto_file.enum_type:
768        enum_node = package_root.find(proto_enum.name)
769        assert enum_node is not None
770        _add_enum_fields(enum_node, proto_enum)
771
772    for message in proto_file.message_type:
773        populate_message(package_root.find(message.name), message)
774
775    for service in proto_file.service:
776        service_node = package_root.find(service.name)
777        assert service_node is not None
778        _add_service_methods(global_root, package_root, service_node, service)
779
780
781def _build_hierarchy(
782    proto_file: descriptor_pb2.FileDescriptorProto,
783) -> tuple[ProtoPackage, ProtoPackage]:
784    """Creates a ProtoNode hierarchy from a proto file descriptor."""
785
786    root = ProtoPackage('')
787    package_root = root
788
789    for part in proto_file.package.split('.'):
790        package = ProtoPackage(part)
791        package_root.add_child(package)
792        package_root = package
793
794    def build_message_subtree(proto_message):
795        node = ProtoMessage(proto_message.name)
796        for proto_enum in proto_message.enum_type:
797            node.add_child(ProtoEnum(proto_enum.name))
798        for submessage in proto_message.nested_type:
799            node.add_child(build_message_subtree(submessage))
800
801        return node
802
803    for proto_enum in proto_file.enum_type:
804        package_root.add_child(ProtoEnum(proto_enum.name))
805
806    for message in proto_file.message_type:
807        package_root.add_child(build_message_subtree(message))
808
809    for service in proto_file.service:
810        package_root.add_child(ProtoService(service.name))
811
812    return root, package_root
813
814
815def build_node_tree(
816    file_descriptor_proto: descriptor_pb2.FileDescriptorProto,
817    proto_options: options.ParsedOptions | None = None,
818) -> tuple[ProtoNode, ProtoNode]:
819    """Constructs a tree of proto nodes from a file descriptor.
820
821    Returns the root node of the entire proto package tree and the node
822    representing the file's package.
823    """
824    global_root, package_root = _build_hierarchy(file_descriptor_proto)
825    _populate_fields(
826        file_descriptor_proto, global_root, package_root, proto_options
827    )
828    return global_root, package_root
829