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