_source_code_info.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. # Copyright (c) 2025-2026 Buf Technologies, Inc.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://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,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. from __future__ import annotations
  15. from typing import TYPE_CHECKING
  16. from protobuf import (
  17. DescComments,
  18. DescEnum,
  19. DescEnumValue,
  20. DescExtension,
  21. DescField,
  22. DescFile,
  23. DescMessage,
  24. DescMethod,
  25. DescOneof,
  26. DescService,
  27. Message,
  28. )
  29. from protobuf.wkt import (
  30. DescriptorProto,
  31. EnumDescriptorProto,
  32. FileDescriptorProto,
  33. ServiceDescriptorProto,
  34. SourceCodeInfo,
  35. )
  36. if TYPE_CHECKING:
  37. from collections.abc import Sequence
  38. def get_package_comments(desc: DescFile) -> DescComments:
  39. """Get comments on the package element in the protobuf source."""
  40. return _find_comments(
  41. desc.proto.source_code_info, [_field_number(FileDescriptorProto, "package")]
  42. )
  43. def get_syntax_comments(desc: DescFile) -> DescComments:
  44. """Get comments on the syntax element in the protobuf source."""
  45. return _find_comments(
  46. desc.proto.source_code_info, [_field_number(FileDescriptorProto, "syntax")]
  47. )
  48. def get_comments(
  49. desc: DescEnum
  50. | DescEnumValue
  51. | DescExtension
  52. | DescField
  53. | DescMessage
  54. | DescMethod
  55. | DescOneof
  56. | DescService,
  57. ) -> DescComments:
  58. """Get comments on the element in the protobuf source."""
  59. match desc:
  60. case DescEnum(parent=parent, file=file, proto=proto):
  61. path = (
  62. [
  63. *get_comments(parent).source_path,
  64. _field_number(DescriptorProto, "enum_type"),
  65. parent.proto.enum_type.index(proto),
  66. ]
  67. if parent is not None
  68. else [
  69. _field_number(FileDescriptorProto, "enum_type"),
  70. file.proto.enum_type.index(proto),
  71. ]
  72. )
  73. case DescOneof(parent=DescMessage(file=file) as parent, proto=proto):
  74. path = [
  75. *get_comments(parent).source_path,
  76. _field_number(DescriptorProto, "oneof_decl"),
  77. parent.proto.oneof_decl.index(proto),
  78. ]
  79. case DescMessage(parent=parent, file=file, proto=proto):
  80. path = (
  81. [
  82. *get_comments(parent).source_path,
  83. _field_number(DescriptorProto, "nested_type"),
  84. parent.proto.nested_type.index(proto),
  85. ]
  86. if parent is not None
  87. else [
  88. _field_number(FileDescriptorProto, "message_type"),
  89. file.proto.message_type.index(proto),
  90. ]
  91. )
  92. case DescEnumValue(parent=DescEnum(file=file) as parent, proto=proto):
  93. path = [
  94. *get_comments(parent).source_path,
  95. _field_number(EnumDescriptorProto, "value"),
  96. parent.proto.value.index(proto),
  97. ]
  98. case DescField(parent=DescMessage(file=file) as parent, proto=proto):
  99. path = [
  100. *get_comments(parent).source_path,
  101. _field_number(DescriptorProto, "field"),
  102. parent.proto.field.index(proto),
  103. ]
  104. case DescExtension(parent=parent, file=file, proto=proto):
  105. path = (
  106. [
  107. *get_comments(parent).source_path,
  108. _field_number(DescriptorProto, "extension"),
  109. parent.proto.extension.index(proto),
  110. ]
  111. if parent is not None
  112. else [
  113. _field_number(FileDescriptorProto, "extension"),
  114. file.proto.extension.index(proto),
  115. ]
  116. )
  117. case DescService(file=file, proto=proto):
  118. path = [
  119. _field_number(FileDescriptorProto, "service"),
  120. file.proto.service.index(proto),
  121. ]
  122. case DescMethod(parent=DescService(file=file) as parent, proto=proto):
  123. path = [
  124. *get_comments(parent).source_path,
  125. _field_number(ServiceDescriptorProto, "method"),
  126. parent.proto.method.index(proto),
  127. ]
  128. case _:
  129. msg = f"unexpected descriptor type: {type(desc)}"
  130. raise TypeError(msg)
  131. return _find_comments(file.proto.source_code_info, path)
  132. def _find_comments(
  133. source_code_info: SourceCodeInfo | None, source_path: Sequence[int]
  134. ) -> DescComments:
  135. """Find comments for a given source path in the source code info."""
  136. location = next(
  137. (
  138. location
  139. for location in (
  140. source_code_info.location if source_code_info is not None else []
  141. )
  142. if location.path == source_path
  143. ),
  144. SourceCodeInfo.Location(),
  145. )
  146. return DescComments(
  147. leading_detached=location.leading_detached_comments,
  148. leading=location.leading_comments
  149. if location.has_field("leading_comments")
  150. else None,
  151. trailing=location.trailing_comments
  152. if location.has_field("trailing_comments")
  153. else None,
  154. source_path=source_path,
  155. )
  156. def _field_number(msg: type[Message], name: str) -> int:
  157. return msg._desc._fields_by_local_name[name].number