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