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