protostar 0.1.0.beta1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +60 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +647 -0
  5. data/THIRD_PARTY_NOTICES.md +61 -0
  6. data/exe/protoc-gen-protostar +7 -0
  7. data/exe/protostar +7 -0
  8. data/lib/protostar/builder/generator.rb +71 -0
  9. data/lib/protostar/builder/sink.rb +35 -0
  10. data/lib/protostar/builder.rb +965 -0
  11. data/lib/protostar/cli.rb +267 -0
  12. data/lib/protostar/codec/decode_generator.rb +199 -0
  13. data/lib/protostar/codec/decoder.rb +296 -0
  14. data/lib/protostar/codec/encode_generator.rb +224 -0
  15. data/lib/protostar/codec/encoder.rb +121 -0
  16. data/lib/protostar/codec/sizer.rb +91 -0
  17. data/lib/protostar/codec.rb +15 -0
  18. data/lib/protostar/compiler/ast.rb +193 -0
  19. data/lib/protostar/compiler/emitters/descriptor_set.rb +57 -0
  20. data/lib/protostar/compiler/emitters/google_ruby.rb +103 -0
  21. data/lib/protostar/compiler/emitters/grpc.rb +175 -0
  22. data/lib/protostar/compiler/emitters/protostar_rbs.rb +252 -0
  23. data/lib/protostar/compiler/emitters/rbs.rb +394 -0
  24. data/lib/protostar/compiler/emitters/ruby.rb +503 -0
  25. data/lib/protostar/compiler/emitters.rb +268 -0
  26. data/lib/protostar/compiler/include/LICENSE +32 -0
  27. data/lib/protostar/compiler/include/google/protobuf/any.proto +106 -0
  28. data/lib/protostar/compiler/include/google/protobuf/api.proto +229 -0
  29. data/lib/protostar/compiler/include/google/protobuf/compiler/plugin.proto +180 -0
  30. data/lib/protostar/compiler/include/google/protobuf/cpp_features.proto +87 -0
  31. data/lib/protostar/compiler/include/google/protobuf/descriptor.proto +1532 -0
  32. data/lib/protostar/compiler/include/google/protobuf/duration.proto +115 -0
  33. data/lib/protostar/compiler/include/google/protobuf/empty.proto +51 -0
  34. data/lib/protostar/compiler/include/google/protobuf/field_mask.proto +243 -0
  35. data/lib/protostar/compiler/include/google/protobuf/java_features.proto +132 -0
  36. data/lib/protostar/compiler/include/google/protobuf/source_context.proto +48 -0
  37. data/lib/protostar/compiler/include/google/protobuf/struct.proto +111 -0
  38. data/lib/protostar/compiler/include/google/protobuf/timestamp.proto +145 -0
  39. data/lib/protostar/compiler/include/google/protobuf/type.proto +217 -0
  40. data/lib/protostar/compiler/include/google/protobuf/wrappers.proto +157 -0
  41. data/lib/protostar/compiler/lexer.rb +659 -0
  42. data/lib/protostar/compiler/linker.rb +894 -0
  43. data/lib/protostar/compiler/option_interpreter.rb +363 -0
  44. data/lib/protostar/compiler/parser.rb +1696 -0
  45. data/lib/protostar/compiler/retention.rb +104 -0
  46. data/lib/protostar/compiler/run.rb +184 -0
  47. data/lib/protostar/compiler/source_info.rb +161 -0
  48. data/lib/protostar/compiler/symbol_table.rb +223 -0
  49. data/lib/protostar/compiler/text_format.rb +340 -0
  50. data/lib/protostar/compiler/validator.rb +944 -0
  51. data/lib/protostar/compiler.rb +304 -0
  52. data/lib/protostar/diagnostics/codes.rb +1307 -0
  53. data/lib/protostar/diagnostics/formats.rb +64 -0
  54. data/lib/protostar/diagnostics/renderer.rb +240 -0
  55. data/lib/protostar/diagnostics/source.rb +153 -0
  56. data/lib/protostar/diagnostics.rb +117 -0
  57. data/lib/protostar/dsl.rb +367 -0
  58. data/lib/protostar/enum.rb +70 -0
  59. data/lib/protostar/errors.rb +23 -0
  60. data/lib/protostar/google/protobuf/any_pb.rb +90 -0
  61. data/lib/protostar/google/protobuf/api_pb.rb +199 -0
  62. data/lib/protostar/google/protobuf/compiler/plugin_pb.rb +168 -0
  63. data/lib/protostar/google/protobuf/descriptor_pb.rb +1239 -0
  64. data/lib/protostar/google/protobuf/duration_pb.rb +99 -0
  65. data/lib/protostar/google/protobuf/empty_pb.rb +36 -0
  66. data/lib/protostar/google/protobuf/field_mask_pb.rb +228 -0
  67. data/lib/protostar/google/protobuf/source_context_pb.rb +33 -0
  68. data/lib/protostar/google/protobuf/struct_pb.rb +91 -0
  69. data/lib/protostar/google/protobuf/timestamp_pb.rb +129 -0
  70. data/lib/protostar/google/protobuf/type_pb.rb +202 -0
  71. data/lib/protostar/google/protobuf/wrappers_pb.rb +125 -0
  72. data/lib/protostar/google_protobuf_shim.rb +24 -0
  73. data/lib/protostar/json/encoder.rb +340 -0
  74. data/lib/protostar/json/errors.rb +34 -0
  75. data/lib/protostar/json/parser.rb +437 -0
  76. data/lib/protostar/json/path.rb +39 -0
  77. data/lib/protostar/json.rb +95 -0
  78. data/lib/protostar/lint/command.rb +197 -0
  79. data/lib/protostar/lint/config.rb +98 -0
  80. data/lib/protostar/lint/context.rb +126 -0
  81. data/lib/protostar/lint/directives.rb +56 -0
  82. data/lib/protostar/lint/fixer.rb +165 -0
  83. data/lib/protostar/lint/naming.rb +54 -0
  84. data/lib/protostar/lint/rule.rb +120 -0
  85. data/lib/protostar/lint/rules/comment_enum.rb +31 -0
  86. data/lib/protostar/lint/rules/comment_enum_value.rb +32 -0
  87. data/lib/protostar/lint/rules/comment_field.rb +33 -0
  88. data/lib/protostar/lint/rules/comment_message.rb +31 -0
  89. data/lib/protostar/lint/rules/comment_rpc.rb +31 -0
  90. data/lib/protostar/lint/rules/comment_service.rb +31 -0
  91. data/lib/protostar/lint/rules/edition_recommended.rb +31 -0
  92. data/lib/protostar/lint/rules/enum_first_value_zero.rb +34 -0
  93. data/lib/protostar/lint/rules/enum_pascal_case.rb +25 -0
  94. data/lib/protostar/lint/rules/enum_value_prefix.rb +38 -0
  95. data/lib/protostar/lint/rules/enum_value_upper_snake_case.rb +28 -0
  96. data/lib/protostar/lint/rules/enum_zero_value_suffix.rb +41 -0
  97. data/lib/protostar/lint/rules/field_lower_snake_case.rb +34 -0
  98. data/lib/protostar/lint/rules/field_not_required.rb +39 -0
  99. data/lib/protostar/lint/rules/field_number_reuse_risk.rb +45 -0
  100. data/lib/protostar/lint/rules/field_numbers_hot_range.rb +44 -0
  101. data/lib/protostar/lint/rules/file_lower_snake_case.rb +27 -0
  102. data/lib/protostar/lint/rules/import_no_public.rb +29 -0
  103. data/lib/protostar/lint/rules/import_no_weak.rb +28 -0
  104. data/lib/protostar/lint/rules/import_used.rb +33 -0
  105. data/lib/protostar/lint/rules/message_pascal_case.rb +27 -0
  106. data/lib/protostar/lint/rules/no_deprecated_features.rb +50 -0
  107. data/lib/protostar/lint/rules/no_groups.rb +34 -0
  108. data/lib/protostar/lint/rules/oneof_lower_snake_case.rb +27 -0
  109. data/lib/protostar/lint/rules/package_defined.rb +28 -0
  110. data/lib/protostar/lint/rules/package_directory_match.rb +33 -0
  111. data/lib/protostar/lint/rules/package_lower_snake_case.rb +28 -0
  112. data/lib/protostar/lint/rules/package_same_directory.rb +39 -0
  113. data/lib/protostar/lint/rules/package_version_suffix.rb +31 -0
  114. data/lib/protostar/lint/rules/reserved_names_with_numbers.rb +44 -0
  115. data/lib/protostar/lint/rules/rpc_pascal_case.rb +25 -0
  116. data/lib/protostar/lint/rules/rpc_request_response_unique.rb +40 -0
  117. data/lib/protostar/lint/rules/rpc_request_standard_name.rb +32 -0
  118. data/lib/protostar/lint/rules/rpc_response_standard_name.rb +31 -0
  119. data/lib/protostar/lint/rules/service_pascal_case.rb +25 -0
  120. data/lib/protostar/lint/rules/service_suffix.rb +28 -0
  121. data/lib/protostar/lint/rules/syntax_specified.rb +35 -0
  122. data/lib/protostar/lint/runner.rb +135 -0
  123. data/lib/protostar/lint.rb +122 -0
  124. data/lib/protostar/message/accessors.rb +86 -0
  125. data/lib/protostar/message/coercion.rb +171 -0
  126. data/lib/protostar/message/field_info.rb +145 -0
  127. data/lib/protostar/message/reflection.rb +149 -0
  128. data/lib/protostar/message.rb +572 -0
  129. data/lib/protostar/plugin.rb +94 -0
  130. data/lib/protostar/schema/default_value.rb +113 -0
  131. data/lib/protostar/schema/descriptor.rb +41 -0
  132. data/lib/protostar/schema/draft/copy.rb +88 -0
  133. data/lib/protostar/schema/draft/resolution.rb +177 -0
  134. data/lib/protostar/schema/draft.rb +779 -0
  135. data/lib/protostar/schema/enum.rb +106 -0
  136. data/lib/protostar/schema/extension_range.rb +37 -0
  137. data/lib/protostar/schema/features.rb +153 -0
  138. data/lib/protostar/schema/field.rb +201 -0
  139. data/lib/protostar/schema/file.rb +217 -0
  140. data/lib/protostar/schema/freezer.rb +61 -0
  141. data/lib/protostar/schema/message.rb +104 -0
  142. data/lib/protostar/schema/oneof.rb +38 -0
  143. data/lib/protostar/schema/options.rb +367 -0
  144. data/lib/protostar/schema/pool.rb +179 -0
  145. data/lib/protostar/schema/proto.rb +576 -0
  146. data/lib/protostar/schema/resolver.rb +118 -0
  147. data/lib/protostar/schema/service.rb +89 -0
  148. data/lib/protostar/schema/source_info.rb +53 -0
  149. data/lib/protostar/schema.rb +65 -0
  150. data/lib/protostar/short_names/any.rb +7 -0
  151. data/lib/protostar/short_names/duration.rb +7 -0
  152. data/lib/protostar/short_names/empty.rb +7 -0
  153. data/lib/protostar/short_names/field_mask.rb +7 -0
  154. data/lib/protostar/short_names/struct.rb +9 -0
  155. data/lib/protostar/short_names/timestamp.rb +7 -0
  156. data/lib/protostar/short_names/wrappers.rb +15 -0
  157. data/lib/protostar/text/extensions.rb +114 -0
  158. data/lib/protostar/text/parser.rb +439 -0
  159. data/lib/protostar/text/printer.rb +304 -0
  160. data/lib/protostar/text/tokenizer.rb +116 -0
  161. data/lib/protostar/text.rb +67 -0
  162. data/lib/protostar/types.rb +152 -0
  163. data/lib/protostar/unknown_field.rb +58 -0
  164. data/lib/protostar/version.rb +5 -0
  165. data/lib/protostar/view/generator.rb +298 -0
  166. data/lib/protostar/view/map.rb +194 -0
  167. data/lib/protostar/view/repeated.rb +143 -0
  168. data/lib/protostar/view/scan.rb +183 -0
  169. data/lib/protostar/view.rb +283 -0
  170. data/lib/protostar/well_known/any.rb +52 -0
  171. data/lib/protostar/well_known/duration.rb +53 -0
  172. data/lib/protostar/well_known/list_value.rb +63 -0
  173. data/lib/protostar/well_known/struct.rb +55 -0
  174. data/lib/protostar/well_known/timestamp.rb +64 -0
  175. data/lib/protostar/well_known/value.rb +60 -0
  176. data/lib/protostar/well_known.rb +38 -0
  177. data/lib/protostar/wire/io_source.rb +40 -0
  178. data/lib/protostar/wire/reader.rb +254 -0
  179. data/lib/protostar/wire/writer.rb +201 -0
  180. data/lib/protostar/wire.rb +96 -0
  181. data/lib/protostar.rb +95 -0
  182. data/shims/google-protobuf/lib/google/protobuf/any_pb.rb +17 -0
  183. data/shims/google-protobuf/lib/google/protobuf/api_pb.rb +22 -0
  184. data/shims/google-protobuf/lib/google/protobuf/compiler/plugin_pb.rb +25 -0
  185. data/shims/google-protobuf/lib/google/protobuf/descriptor_pb.rb +72 -0
  186. data/shims/google-protobuf/lib/google/protobuf/descriptor_pool.rb +164 -0
  187. data/shims/google-protobuf/lib/google/protobuf/descriptors.rb +553 -0
  188. data/shims/google-protobuf/lib/google/protobuf/duration_pb.rb +17 -0
  189. data/shims/google-protobuf/lib/google/protobuf/empty_pb.rb +17 -0
  190. data/shims/google-protobuf/lib/google/protobuf/field_mask_pb.rb +17 -0
  191. data/shims/google-protobuf/lib/google/protobuf/internal/convert.rb +180 -0
  192. data/shims/google-protobuf/lib/google/protobuf/internal/extensions.rb +154 -0
  193. data/shims/google-protobuf/lib/google/protobuf/internal/json.rb +104 -0
  194. data/shims/google-protobuf/lib/google/protobuf/internal/well_known.rb +140 -0
  195. data/shims/google-protobuf/lib/google/protobuf/map.rb +216 -0
  196. data/shims/google-protobuf/lib/google/protobuf/message.rb +544 -0
  197. data/shims/google-protobuf/lib/google/protobuf/message_exts.rb +41 -0
  198. data/shims/google-protobuf/lib/google/protobuf/plugin_pb.rb +4 -0
  199. data/shims/google-protobuf/lib/google/protobuf/repeated_field.rb +366 -0
  200. data/shims/google-protobuf/lib/google/protobuf/source_context_pb.rb +17 -0
  201. data/shims/google-protobuf/lib/google/protobuf/struct_pb.rb +20 -0
  202. data/shims/google-protobuf/lib/google/protobuf/timestamp_pb.rb +17 -0
  203. data/shims/google-protobuf/lib/google/protobuf/type_pb.rb +27 -0
  204. data/shims/google-protobuf/lib/google/protobuf/well_known_types.rb +37 -0
  205. data/shims/google-protobuf/lib/google/protobuf/wrappers_pb.rb +25 -0
  206. data/shims/google-protobuf/lib/google/protobuf.rb +107 -0
  207. data/shims/google-protobuf/lib/google/protobuf_ffi.rb +5 -0
  208. data/shims/google-protobuf/lib/google/protobuf_native.rb +5 -0
  209. data/sig/protostar/google/protobuf/any_pb.rbs +77 -0
  210. data/sig/protostar/google/protobuf/api_pb.rbs +242 -0
  211. data/sig/protostar/google/protobuf/compiler/plugin_pb.rbs +264 -0
  212. data/sig/protostar/google/protobuf/descriptor_pb.rbs +2220 -0
  213. data/sig/protostar/google/protobuf/duration_pb.rbs +82 -0
  214. data/sig/protostar/google/protobuf/empty_pb.rbs +18 -0
  215. data/sig/protostar/google/protobuf/field_mask_pb.rbs +170 -0
  216. data/sig/protostar/google/protobuf/source_context_pb.rbs +24 -0
  217. data/sig/protostar/google/protobuf/struct_pb.rbs +135 -0
  218. data/sig/protostar/google/protobuf/timestamp_pb.rbs +102 -0
  219. data/sig/protostar/google/protobuf/type_pb.rbs +304 -0
  220. data/sig/protostar/google/protobuf/wrappers_pb.rbs +169 -0
  221. data/sig/protostar/lint.rbs +216 -0
  222. data/sig/protostar.rbs +326 -0
  223. metadata +366 -0
@@ -0,0 +1,944 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Protostar
4
+ class Compiler
5
+ # The checks protoc runs once a file's options are interpreted:
6
+ # DescriptorBuilder's ValidateOptions phase, map entry conflict hints,
7
+ # unused imports, naming style, proto limits and symbol visibility, and
8
+ # feature lifetimes, in that order. Checks read the built {Schema::File}
9
+ # and report against the AST's recorded positions.
10
+ #
11
+ # @api private
12
+ class Validator
13
+ INT32_MAX = 2**31 - 1
14
+ MAX_FIELD_NUMBER = 536_870_911
15
+ OPTIONS_EXTENDEES = %w[FileOptions MessageOptions FieldOptions EnumOptions EnumValueOptions ServiceOptions
16
+ MethodOptions OneofOptions ExtensionRangeOptions].flat_map { |n| ["google.protobuf.#{n}", "proto2.#{n}"] }.freeze
17
+ DESCRIPTOR_OPTIONS = OPTIONS_EXTENDEES.select { |n| n.start_with?("google.") }.freeze
18
+ INT64_TYPES = %i[int64 uint64 sint64 fixed64 sfixed64].freeze
19
+ NON_MESSAGE_TYPES = %w[double float int64 uint64 int32 fixed32 fixed64 bool string bytes uint32 enum sfixed32
20
+ sfixed64 sint32 sint64].freeze
21
+ NAMING_OPT_OUT = " (features.enforce_naming_style = STYLE_LEGACY can be used to opt out of this check)"
22
+ COLLISION_OPT_OUT = " (features.enforce_naming_style = STYLE2024 can be used to opt out of this check)"
23
+ LIMITS_OPT_OUT = " (features.enforce_proto_limits = LEGACY_NO_EXPLICIT_LIMITS can be used to opt out of this check)"
24
+ LIMIT_FIELDS_PER_MESSAGE = 65_535
25
+ LIMIT_ONEOFS_PER_MESSAGE = 255
26
+ LIMIT_FIELDS_PER_ONEOF = 255
27
+ LIMIT_VALUES_PER_ENUM = 65_535
28
+ TARGET_TYPES = {
29
+ file: [:TARGET_TYPE_FILE, "file"], extension_range: [:TARGET_TYPE_EXTENSION_RANGE, "extension range"],
30
+ message: [:TARGET_TYPE_MESSAGE, "message"], field: [:TARGET_TYPE_FIELD, "field"],
31
+ oneof: [:TARGET_TYPE_ONEOF, "oneof"], enum: [:TARGET_TYPE_ENUM, "enum"],
32
+ enum_value: [:TARGET_TYPE_ENUM_ENTRY, "enum entry"], service: [:TARGET_TYPE_SERVICE, "service"],
33
+ method: [:TARGET_TYPE_METHOD, "method"]
34
+ }.freeze
35
+
36
+ def initialize(linker)
37
+ @linker = linker
38
+ @file = linker.file
39
+ @ast = linker.ast
40
+ @cli = linker.protoc_cli?
41
+ end
42
+
43
+ def validate
44
+ validate_options if @file && !errors?
45
+ @ast.message_type.each { |m| detect_map_conflicts(m) } if errors?
46
+ return if errors?
47
+ unused_imports
48
+ each_element { |node, kind| naming_style(node, kind) } if @cli
49
+ each_element { |node, kind| proto_limits(node, kind) } if @cli
50
+ symbol_visibility if @cli
51
+ feature_lifetimes unless errors?
52
+ end
53
+
54
+ # The command-line interface's checks over the files it was asked to
55
+ # compile, once all of them are built
56
+ # @param linkers [Array<Linker>]
57
+ # @param run [Run]
58
+ def self.cli_checks(linkers, run)
59
+ linkers.each do |linker|
60
+ file = linker.file
61
+ options = file.options
62
+ if options.cc_generic_services? || options.java_generic_services? || options.py_generic_services?
63
+ run.report(file.name, nil, "Generic services (cc_generic_services, java_generic_services, and " \
64
+ "py_generic_services) are deprecated in favor of using plugins that generate code specific to your " \
65
+ "particular RPC system. Additional code generator options may be required to enable generic services " \
66
+ "and total removal of these options is planned in future breaking releases.", :warning)
67
+ end
68
+ linker.each_element do |node, kind|
69
+ next unless kind == :field
70
+ next unless node.number.between?(19_000, 19_999)
71
+ run.report(file.name, nil, "Field numbers 19000 through 19999 are reserved for the protocol buffer " \
72
+ "library implementation.")
73
+ end
74
+ end
75
+ linkers.each do |linker|
76
+ validator = new(linker)
77
+ linker.each_element { |node, kind| validator.target_constraints(node, kind, run) }
78
+ end
79
+ return if run.failed?
80
+ linkers.each do |linker|
81
+ file = linker.file
82
+ next unless file.syntax == :editions
83
+ next if skip_edition_check?(file.name)
84
+ next unless Schema::Features::EDITIONS.fetch(file.edition) > Schema::Features::EDITIONS[:EDITION_2026]
85
+ run.report(file.name, nil, "is a file using edition #{file.edition.to_s.delete_prefix("EDITION_")}, which " \
86
+ "is later than the protoc maximum supported edition 2026.")
87
+ break
88
+ end
89
+ end
90
+
91
+ def self.skip_edition_check?(name)
92
+ name.start_with?("google/protobuf/", "upb/", "com/google/protobuf/", "conformance/test_protos/",
93
+ "test_messages_edition_unstable")
94
+ end
95
+
96
+ # Every option set on the element must allow the element's target type
97
+ def target_constraints(node, kind, run)
98
+ options = schema_options(node, kind)
99
+ return if options.nil? || options.empty?
100
+ type = options_message(kind)
101
+ return unless type
102
+ target, label = TARGET_TYPES.fetch(kind)
103
+ check_targets(options, type, target, label, run)
104
+ end
105
+
106
+ private
107
+
108
+ def errors?
109
+ @linker.errors?
110
+ end
111
+
112
+ def error(node, location, message, related: [])
113
+ @linker.error(node, location, message, related: related)
114
+ end
115
+
116
+ def warning(node, location, message, related: [])
117
+ @linker.warning(node, location, message, related: related)
118
+ end
119
+
120
+ def each_element(&block)
121
+ @linker.each_element(&block)
122
+ end
123
+
124
+ def check_targets(value, type, target, label, run)
125
+ value.fields.each do |name, v|
126
+ field = type.fields_by_name[name.to_s]
127
+ check_target(field, v, target, label, run) if field
128
+ end
129
+ value.extensions.each { |field, v| check_target(field, v, target, label, run) }
130
+ end
131
+
132
+ def check_target(field, value, target, label, run)
133
+ targets = field.options.targets
134
+ unless targets.empty? || targets.include?(target)
135
+ run.report(@ast.name, nil, "Option #{field.full_name} cannot be set on an entity of type `#{label}`.")
136
+ end
137
+ return unless field.type == :message || field.type == :group
138
+ Array(field.repeated? ? value : [value]).each do |v|
139
+ check_targets(v, field.message_type, target, label, run) if v.is_a?(Schema::MessageValue)
140
+ end
141
+ end
142
+
143
+ # A built extension by full name: this file's or the pool's
144
+ def extension_field(name)
145
+ found = @file.find_symbol(name) || @linker.pool.find_symbol(name)
146
+ found if found.is_a?(Schema::Field) && found.extension?
147
+ end
148
+
149
+ # The options message an element's options are read with
150
+ def options_message(kind)
151
+ name = Linker::OPTIONS_TYPES.fetch(kind)
152
+ @file.find_message(name) || @linker.interpreter&.options_type(name)
153
+ end
154
+
155
+ # The Schema descriptor for an AST node
156
+ def schema_of(node, _kind)
157
+ @linker.descriptor_of(node)
158
+ end
159
+
160
+ def schema_options(node, kind)
161
+ return nil unless node.options
162
+ schema_of(node, kind).options
163
+ end
164
+
165
+ def validate_options
166
+ each_element do |node, kind|
167
+ case kind
168
+ when :file then validate_file
169
+ when :message then validate_message(node, schema_of(node, kind))
170
+ when :field then validate_field(node, schema_of(node, kind))
171
+ when :enum then validate_enum(node, schema_of(node, kind))
172
+ when :enum_value then validate_enum_value(node, schema_of(node, kind))
173
+ when :service then validate_service(node)
174
+ end
175
+ end
176
+ end
177
+
178
+ def lite?(file)
179
+ file && file.options.optimize_for == :LITE_RUNTIME
180
+ end
181
+
182
+ def validate_file
183
+ unless @linker.legacy?
184
+ if @file.features.field_presence == :LEGACY_REQUIRED
185
+ error(@ast, :editions, "Required presence can't be specified by default.")
186
+ end
187
+ if @file.options.java_string_check_utf8?
188
+ error(@ast, :editions, "File option java_string_check_utf8 is not allowed under editions. Use the " \
189
+ "(pb.java).utf8_validation feature to control this behavior.")
190
+ end
191
+ end
192
+ unless lite?(@file)
193
+ dep = @file.dependency_files.find { |d| lite?(d) }
194
+ if dep
195
+ @linker.import_error(dep.name, "Files that do not use optimize_for = LITE_RUNTIME cannot import files " \
196
+ "which do use this option. This file is not lite, but it imports \"#{dep.name}\" which is.")
197
+ end
198
+ end
199
+ validate_proto3 if @linker.edition == :EDITION_PROTO3
200
+ edition = @linker.edition_number
201
+ if edition < 1001 && !@ast.option_dependency.empty?
202
+ @linker.import_error("option", "option imports are not supported before edition 2024.")
203
+ end
204
+ if edition >= 1001 && !@ast.weak_dependency.empty?
205
+ @linker.import_error("weak", "weak imports are not allowed under edition 2024 and beyond.")
206
+ end
207
+ end
208
+
209
+ def validate_proto3
210
+ @ast.extension.each { |f| validate_proto3_field(f) }
211
+ @ast.message_type.each { |m| validate_proto3_message(m) }
212
+ end
213
+
214
+ def validate_proto3_message(message)
215
+ message.nested_type.each { |m| validate_proto3_message(m) }
216
+ message.field.each { |f| validate_proto3_field(f) }
217
+ message.extension.each { |f| validate_proto3_field(f) }
218
+ unless message.extension_range.empty?
219
+ error(message.extension_range[0], :number, "Extension ranges are not allowed in proto3.")
220
+ end
221
+ if @file.find_message(message.full_name).message_set_wire_format?
222
+ error(message, :name, "MessageSet is not supported in proto3.")
223
+ end
224
+ end
225
+
226
+ def validate_proto3_field(node)
227
+ field = @file.find_symbol(node.full_name)
228
+ if field.extension? && !OPTIONS_EXTENDEES.include?(field.extendee.full_name)
229
+ error(node, :extendee, "Extensions in proto3 are only allowed for defining options.")
230
+ end
231
+ error(node, :type, "Required fields are not allowed in proto3.") if field.required?
232
+ error(node, :default_value, "Explicit default values are not allowed in proto3.") if node.default_value
233
+ if field.type == :enum && field.enum_type.closed?
234
+ holder = field.extension? ? field.extendee : field.containing_message
235
+ error(node, :type, "Enum type \"#{field.enum_type.full_name}\" is not an open enum, but is used in " \
236
+ "\"#{holder.full_name}\" which is a proto3 message type.")
237
+ end
238
+ error(node, :type, "Groups are not supported in proto3 syntax.") if field.type == :group
239
+ end
240
+
241
+ def validate_message(node, message)
242
+ unless message.options.deprecated_legacy_json_field_conflicts?
243
+ json_name_uniqueness(node, message, false)
244
+ json_name_uniqueness(node, message, true)
245
+ end
246
+ extension_range_options(node, message)
247
+ end
248
+
249
+ def json_name_uniqueness(node, message, use_custom)
250
+ seen = {}
251
+ node.field.each do |field|
252
+ default = Schema.json_name(field.name)
253
+ custom = use_custom && field.json_name && field.json_name != default
254
+ name = custom ? field.json_name : default
255
+ if custom && name.start_with?("[") && name.end_with?("]")
256
+ error(field, :name, "The custom JSON name of field \"#{field.name}\" (\"#{name}\") is invalid: JSON " \
257
+ "names may not start with '[' and end with ']'.")
258
+ next
259
+ end
260
+ unless seen.key?(name)
261
+ seen[name] = [field, name, custom]
262
+ next
263
+ end
264
+ match, match_name, match_custom = seen[name]
265
+ next if use_custom && !custom && !match_custom
266
+ suffix = (name != match_name) ? " (\"#{match_name}\")" : ""
267
+ text = "The #{custom ? "custom" : "default"} JSON name of field \"#{field.name}\" (\"#{name}\") conflicts " \
268
+ "with the #{match_custom ? "custom" : "default"} JSON name of field \"#{match.name}\"#{suffix}."
269
+ related = [[match, :name, "`#{match.name}` has it too"]]
270
+ if message.features.json_format == :LEGACY_BEST_EFFORT && (!custom || !match_custom)
271
+ warning(field, :name, text, related: related)
272
+ else
273
+ error(field, :name, text, related: related)
274
+ end
275
+ end
276
+ end
277
+
278
+ def extension_range_options(node, message)
279
+ max = message.message_set_wire_format? ? INT32_MAX : MAX_FIELD_NUMBER
280
+ names = {}
281
+ message.extension_range_descriptors.each_with_index do |range, i|
282
+ if range.range.end + 1 > max + 1
283
+ error(node, :number, "Extension numbers cannot be greater than #{max}.")
284
+ end
285
+ declarations = range.options.declaration
286
+ next if declarations.empty?
287
+ range_node = node.extension_range[i]
288
+ if range.options.verification == :UNVERIFIED
289
+ error(range_node, :extendee, "Cannot mark the extension range as UNVERIFIED when it has extension(s) " \
290
+ "declared.")
291
+ break
292
+ end
293
+ validate_declarations(range_node, range, declarations, names)
294
+ end
295
+ end
296
+
297
+ def validate_declarations(node, range, declarations, names)
298
+ numbers = {}
299
+ declarations.each do |d|
300
+ number = d[:number]
301
+ unless range.range.cover?(number)
302
+ error(node, :number, "Extension declaration number #{number} is not in the extension range.")
303
+ end
304
+ if numbers[number]
305
+ error(node, :number, "Extension declaration number #{number} is declared multiple times.")
306
+ end
307
+ numbers[number] = true
308
+ if !d.key?(:full_name) || !d.key?(:type)
309
+ if d.key?(:full_name) != d.key?(:type) || !d[:reserved]
310
+ error(node, :extendee, "Extension declaration ##{number} should have both \"full_name\" and \"type\" set.")
311
+ end
312
+ else
313
+ if names[d[:full_name]]
314
+ error(node, :name, "Extension field name \"#{d[:full_name]}\" is declared multiple times.")
315
+ break
316
+ end
317
+ names[d[:full_name]] = true
318
+ if (problem = declaration_symbol_problem(d[:full_name]))
319
+ error(node, :name, problem)
320
+ end
321
+ if !NON_MESSAGE_TYPES.include?(d[:type]) && (problem = declaration_symbol_problem(d[:type]))
322
+ error(node, :name, problem)
323
+ end
324
+ end
325
+ end
326
+ end
327
+
328
+ def declaration_symbol_problem(symbol)
329
+ return "\"#{symbol}\" must have a leading dot to indicate the fully-qualified scope." unless symbol.start_with?(".")
330
+ unless symbol[1..].match?(/\A[A-Za-z0-9_]+(\.[A-Za-z0-9_]+)*\z/)
331
+ return "\"#{symbol}\" contains invalid identifiers."
332
+ end
333
+ nil
334
+ end
335
+
336
+ def validate_field(node, field)
337
+ if @cli
338
+ problem = feature_support_problem(field.options.feature_support, field.full_name)
339
+ if problem.nil? && field.type == :enum
340
+ field.enum_type.values.each do |value|
341
+ problem = enum_value_support_problem(field.options.feature_support, value, field.full_name)
342
+ break if problem
343
+ end
344
+ end
345
+ error(node, :option_name, problem) if problem
346
+ end
347
+ field_features(node, field)
348
+ options = field.options
349
+ if @linker.edition_number >= 1001 && options.key?(:ctype)
350
+ error(node, :type, "ctype option is not allowed under edition 2024 and beyond. Use the feature " \
351
+ "string_type = VIEW|CORD|STRING|... instead.")
352
+ end
353
+ if options.lazy? || options.unverified_lazy?
354
+ if field.type != :message
355
+ error(node, :type, "[lazy = true] can only be specified for submessage fields.")
356
+ end
357
+ if options.unverified_lazy? && field.extension?
358
+ error(node, :type, "Extensions cannot be marked as unverified_lazy.")
359
+ end
360
+ end
361
+ if options.packed? && !field.packable?
362
+ error(node, :type, "[packed = true] can only be specified for repeated primitive fields.")
363
+ end
364
+ holder = field.extension? ? field.extendee : field.containing_message
365
+ if holder&.message_set_wire_format?
366
+ if field.extension?
367
+ if field.required? || field.repeated? || field.type != :message
368
+ error(node, :type, "Extensions of MessageSets must be optional messages.")
369
+ end
370
+ else
371
+ error(node, :name, "MessageSets cannot have fields, only extensions.")
372
+ end
373
+ end
374
+ if lite?(@file) && field.extension? && !lite?(field.extendee.file)
375
+ error(node, :extendee, "Extensions to non-lite types can only be declared in non-lite files. Note that " \
376
+ "you cannot extend a non-lite type to contain a lite type, but the reverse is allowed.")
377
+ end
378
+ if field.type == :group && field.message_type&.map_entry?
379
+ error(node, :type, "Groups cannot be map entries. Use a regular message field with map<KeyType, " \
380
+ "ValueType> syntax instead.")
381
+ end
382
+ validate_map(node, field) if field.type == :message && field.message_type.map_entry?
383
+ jstype(node, field)
384
+ if field.extension? && node.json_name && node.json_name != Schema.json_name(node.name)
385
+ error(node, :option_name, "option json_name is not allowed on extension fields.")
386
+ end
387
+ if field.json_name.include?("\0")
388
+ error(node, :option_name, "json_name cannot have embedded null characters.")
389
+ end
390
+ extension_declaration(node, field) if field.extension? && @cli
391
+ end
392
+
393
+ def field_features(node, field)
394
+ return if @linker.edition_number < 1000
395
+ if node.label == :LABEL_REQUIRED
396
+ error(node, :name, "Required label is not allowed under editions. Use the feature field_presence = " \
397
+ "LEGACY_REQUIRED to control this behavior.")
398
+ end
399
+ if node.type == :TYPE_GROUP
400
+ error(node, :name, "Group types are not allowed under editions. Use the feature message_encoding = " \
401
+ "DELIMITED to control this behavior.")
402
+ end
403
+ if field.options.key?(:packed)
404
+ error(node, :name, "Field option packed is not allowed under editions. Use the repeated_field_encoding " \
405
+ "feature to control this behavior.")
406
+ end
407
+ if !field.repeated? && !field.presence?
408
+ error(node, :name, "Implicit presence fields can't specify defaults.") if node.default_value
409
+ if field.type == :enum && field.enum_type.closed?
410
+ error(node, :name, "Implicit presence enum fields must always be open.")
411
+ end
412
+ end
413
+ error(node, :name, "Extensions can't be required.") if field.extension? && field.required?
414
+ return if !field.extension? && field.containing_message.map_entry?
415
+ raw = field.options[:features] || Schema::MessageValue::EMPTY
416
+ if raw.key?(:field_presence)
417
+ if field.real_oneof || field.oneof
418
+ error(node, :name, "Oneof fields can't specify field presence.")
419
+ elsif field.repeated?
420
+ error(node, :name, "Repeated fields can't specify field presence.")
421
+ elsif field.extension? && raw[:field_presence] != :LEGACY_REQUIRED
422
+ error(node, :name, "Extensions can't specify field presence.")
423
+ elsif field.message_type && raw[:field_presence] == :IMPLICIT
424
+ error(node, :name, "Message fields can't specify implicit presence.")
425
+ end
426
+ end
427
+ if !field.repeated? && raw.key?(:repeated_field_encoding)
428
+ error(node, :name, "Only repeated fields can specify repeated field encoding.")
429
+ end
430
+ string_map = field.map? && field.message_type.fields.any? { |f| f.type == :string }
431
+ if field.type != :string && !string_map && raw.key?(:utf8_validation)
432
+ error(node, :name, "Only string fields can specify utf8 validation.")
433
+ end
434
+ if !field.packable? && raw[:repeated_field_encoding] == :PACKED
435
+ error(node, :name, "Only repeated primitive fields can specify PACKED repeated field encoding.")
436
+ end
437
+ message_like = (field.type == :message || field.type == :group) && !field.message_type.map_entry?
438
+ if !message_like && raw.key?(:message_encoding)
439
+ error(node, :name, "Only message fields can specify message encoding.")
440
+ end
441
+ end
442
+
443
+ def validate_map(node, field)
444
+ entry = field.message_type
445
+ expected = camel_case(field.name) + "Entry"
446
+ if !field.repeated? || entry.name != expected || !entry.containing_message.equal?(field.containing_message)
447
+ text = "#{entry.full_name} is a synthetic MapEntry message type, which is not allowed to be used as a " \
448
+ "field type."
449
+ if (container = entry.containing_message)
450
+ alternative = @linker.table.lookup(entry.name, container.full_name, types_only: true)
451
+ if alternative && alternative.full_name != entry.full_name
452
+ text += " Maybe you meant \".#{alternative.full_name}\"?"
453
+ end
454
+ end
455
+ error(node, :type, text)
456
+ end
457
+ unless valid_map_entry?(node, entry)
458
+ error(node, :type, "map_entry should not be set explicitly. Use map<KeyType, ValueType> instead.")
459
+ end
460
+ end
461
+
462
+ def camel_case(name)
463
+ out = +""
464
+ upper = true
465
+ name.each_char do |c|
466
+ if c == "_"
467
+ upper = true
468
+ elsif upper
469
+ out << c.upcase(:ascii)
470
+ upper = false
471
+ else
472
+ out << c
473
+ end
474
+ end
475
+ out
476
+ end
477
+
478
+ def valid_map_entry?(node, entry)
479
+ return false unless entry.extensions.empty? && entry.extension_ranges.empty? && entry.nested_messages.empty? &&
480
+ entry.nested_enums.empty? && entry.fields.size == 2
481
+ key, value = entry.fields
482
+ return false unless key.label == :optional && key.number == 1 && key.name == "key"
483
+ return false unless value.label == :optional && value.number == 2 && value.name == "value"
484
+ case key.type
485
+ when :enum
486
+ error(node, :type, "Key in map fields cannot be enum types.")
487
+ when :float, :double, :message, :group, :bytes
488
+ error(node, :type, "Key in map fields cannot be float/double, bytes or message types.")
489
+ end
490
+ if value.type == :enum && value.enum_type.values.first.number != 0
491
+ error(node, :type, "Enum value in map must define 0 as the first value.")
492
+ end
493
+ true
494
+ end
495
+
496
+ def jstype(node, field)
497
+ jstype = field.options.jstype
498
+ return if jstype.nil? || jstype == :JS_NORMAL
499
+ if INT64_TYPES.include?(field.type)
500
+ return if jstype == :JS_STRING || jstype == :JS_NUMBER
501
+ error(node, :type, "Illegal jstype for int64, uint64, sint64, fixed64 or sfixed64 field: #{jstype}")
502
+ else
503
+ error(node, :type, "jstype is only allowed on int64, uint64, sint64, fixed64 or sfixed64 fields.")
504
+ end
505
+ end
506
+
507
+ def extension_declaration(node, field)
508
+ extendee = field.extendee
509
+ return if DESCRIPTOR_OPTIONS.include?(extendee.full_name)
510
+ return if extendee.file.name == "google/protobuf/descriptor.proto"
511
+ range = extendee.extension_range_descriptors.find { |r| r.range.cover?(field.number) }
512
+ return if range.nil? || !range.options.declared?
513
+ declarations = range.options.declaration
514
+ declaration = declarations.find { |d| d[:number] == field.number }
515
+ if declaration&.[](:reserved)
516
+ error(node, :extendee, "Cannot use number #{field.number} for extension field #{field.full_name}, as it " \
517
+ "is reserved in the extension declarations for message #{extendee.full_name}.")
518
+ elsif declaration
519
+ check_declaration(node, field, declaration)
520
+ elsif !declarations.empty? || range.options.verification == :DECLARATION
521
+ error(node, :extendee, "Missing extension declaration for field #{field.full_name} with number " \
522
+ "#{field.number} in extendee message #{extendee.full_name}. An extension range must declare for all " \
523
+ "extension fields if its verification state is DECLARATION or there's any declaration in the range " \
524
+ "already. Otherwise, consider splitting up the range.")
525
+ end
526
+ end
527
+
528
+ def check_declaration(node, field, declaration)
529
+ extendee = field.extendee.full_name
530
+ if (type = declaration[:type]) && !type.empty?
531
+ actual = if field.message_type || field.enum_type
532
+ ".#{(field.message_type || field.enum_type).full_name}"
533
+ else
534
+ field.type.to_s
535
+ end
536
+ expected = (NON_MESSAGE_TYPES.include?(type) || type.start_with?(".")) ? type : ".#{type}"
537
+ if expected != actual
538
+ error(node, :extendee, "\"#{extendee}\" extension field #{field.number} is expected to be type " \
539
+ "\"#{expected}\", not \"#{actual}\".")
540
+ end
541
+ end
542
+ if (name = declaration[:full_name]) && !name.empty? && name != ".#{field.full_name}"
543
+ error(node, :extendee, "\"#{extendee}\" extension field #{field.number} is expected to have field name " \
544
+ "\"#{name}\", not \".#{field.full_name}\".")
545
+ end
546
+ repeated = declaration[:repeated] == true
547
+ if repeated != field.repeated?
548
+ error(node, :extendee, "\"#{extendee}\" extension field #{field.number} is expected to be " \
549
+ "#{repeated ? "repeated" : "optional"}.")
550
+ end
551
+ end
552
+
553
+ def validate_enum(node, enum)
554
+ enum_value_uniqueness(node, enum)
555
+ if enum.open? && !enum.values.empty? && enum.values.first.number != 0
556
+ error(node.value[0], :number, "The first enum value must be zero for open enums.")
557
+ end
558
+ return if enum.options.allow_alias?
559
+ used = {}
560
+ numbers = enum.values.map(&:number)
561
+ enum.values.each_with_index do |value, i|
562
+ if (first, first_index = used[value.number])
563
+ next_value = value.number + 1
564
+ next_value += 1 while numbers.include?(next_value)
565
+ text = "\"#{value.full_name}\" uses the same enum value as \"#{first}\". If this is intended, set " \
566
+ "'option allow_alias = true;' to the enum definition."
567
+ text += " The next available enum value is #{next_value}." if next_value < INT32_MAX
568
+ error(node.value[i], :number, text, related: [[node.value[first_index], :number, "first used by `#{node.value[first_index].name}`"]])
569
+ else
570
+ used[value.number] = [value.full_name, i]
571
+ end
572
+ end
573
+ end
574
+
575
+ def enum_value_uniqueness(node, enum)
576
+ prefix = enum.name.delete("_").downcase(:ascii)
577
+ seen = {}
578
+ enum.values.each_with_index do |value, i|
579
+ stripped = pascal_case(remove_prefix(prefix, value.name))
580
+ if (other = seen[stripped])
581
+ next if other.name == value.name || other.number == value.number
582
+ text = "Enum name #{value.name} has the same name as #{other.name} if you ignore case and strip out the " \
583
+ "enum name prefix (if any). (If you are using allow_alias, please assign the same number to each enum " \
584
+ "value name.)"
585
+ if enum.options.deprecated_legacy_json_field_conflicts? && @linker.edition == :EDITION_PROTO2
586
+ warning(node.value[i], :name, text)
587
+ else
588
+ error(node.value[i], :name, text)
589
+ end
590
+ else
591
+ seen[stripped] = value
592
+ end
593
+ end
594
+ end
595
+
596
+ def remove_prefix(prefix, name)
597
+ i = 0
598
+ j = 0
599
+ while i < name.size && j < prefix.size
600
+ if name[i] == "_"
601
+ i += 1
602
+ next
603
+ end
604
+ return name if name[i].downcase(:ascii) != prefix[j]
605
+ i += 1
606
+ j += 1
607
+ end
608
+ return name if j < prefix.size
609
+ i += 1 while i < name.size && name[i] == "_"
610
+ return name if i == name.size
611
+ name[i..]
612
+ end
613
+
614
+ def pascal_case(name)
615
+ out = +""
616
+ upper = true
617
+ name.each_char do |c|
618
+ if c == "_"
619
+ upper = true
620
+ else
621
+ out << (upper ? c.upcase(:ascii) : c.downcase(:ascii))
622
+ upper = false
623
+ end
624
+ end
625
+ out
626
+ end
627
+
628
+ def validate_enum_value(node, value)
629
+ return unless @cli
630
+ problem = feature_support_problem(value.options.feature_support, value.full_name)
631
+ error(node, :option_name, problem) if problem
632
+ end
633
+
634
+ def validate_service(node)
635
+ return unless lite?(@file)
636
+ return unless @file.options.cc_generic_services? || @file.options.java_generic_services?
637
+ error(node, :name, "Files with optimize_for = LITE_RUNTIME cannot define services unless you set both " \
638
+ "options cc_generic_services and java_generic_services to false.")
639
+ end
640
+
641
+ def edition_value(name)
642
+ Schema::Features::EDITIONS.fetch(name, 0)
643
+ end
644
+
645
+ # FeatureResolver::ValidateFeatureSupport
646
+ def feature_support_problem(support, full_name)
647
+ return nil if support.nil?
648
+ if support.key?(:edition_deprecated)
649
+ if edition_value(support[:edition_deprecated]) < edition_value(support[:edition_introduced])
650
+ return "#{full_name} was deprecated before it was introduced."
651
+ end
652
+ unless support.key?(:deprecation_warning)
653
+ return "#{full_name} is deprecated but does not specify a deprecation warning."
654
+ end
655
+ end
656
+ if !support.key?(:edition_deprecated) && support.key?(:deprecation_warning)
657
+ return "#{full_name} specifies a deprecation warning but is not marked deprecated in any edition."
658
+ end
659
+ if support.key?(:edition_removed)
660
+ if edition_value(support[:edition_deprecated]) >= edition_value(support[:edition_removed])
661
+ return "#{full_name} was deprecated after it was removed."
662
+ end
663
+ if edition_value(support[:edition_removed]) < edition_value(support[:edition_introduced])
664
+ return "#{full_name} was removed before it was introduced."
665
+ end
666
+ if support[:edition_introduced] != support[:edition_removed] && !support.key?(:removal_error)
667
+ return "#{full_name} has been removed but does not specify a removal error."
668
+ end
669
+ elsif support.key?(:removal_error)
670
+ return "#{full_name} specifies a removal error but is not marked removed in any edition."
671
+ end
672
+ nil
673
+ end
674
+
675
+ def enum_value_support_problem(parent, value, field_name)
676
+ own = value.options.feature_support
677
+ return nil if own.nil? || parent.nil?
678
+ support = parent.merge(own)
679
+ problem = feature_support_problem(support, value.full_name)
680
+ return problem if problem
681
+ if edition_value(support[:edition_introduced]) < edition_value(parent[:edition_introduced])
682
+ return "value #{value.full_name} was introduced before #{field_name} was."
683
+ end
684
+ if parent.key?(:edition_removed) && edition_value(support[:edition_removed]) > edition_value(parent[:edition_removed])
685
+ return "value #{value.full_name} was removed after #{field_name} was."
686
+ end
687
+ if parent.key?(:edition_deprecated) &&
688
+ edition_value(support[:edition_deprecated]) > edition_value(parent[:edition_deprecated])
689
+ return "value #{value.full_name} was deprecated after #{field_name} was."
690
+ end
691
+ nil
692
+ end
693
+
694
+ def detect_map_conflicts(message)
695
+ seen = {}
696
+ message.nested_type.each do |nested|
697
+ if (other = seen[nested.name])
698
+ if map_entry?(other) || map_entry?(nested)
699
+ error(message, :name, "Expanded map entry type #{nested.name} conflicts with an existing nested " \
700
+ "message type.")
701
+ break
702
+ end
703
+ else
704
+ seen[nested.name] = nested
705
+ end
706
+ detect_map_conflicts(nested)
707
+ end
708
+ message.field.each do |field|
709
+ other = seen[field.name]
710
+ if other && map_entry?(other)
711
+ error(message, :name, "Expanded map entry type #{other.name} conflicts with an existing field.")
712
+ end
713
+ end
714
+ message.enum_type.each do |enum|
715
+ other = seen[enum.name]
716
+ if other && map_entry?(other)
717
+ error(message, :name, "Expanded map entry type #{other.name} conflicts with an existing enum type.")
718
+ end
719
+ end
720
+ message.oneof_decl.each do |oneof|
721
+ other = seen[oneof.name]
722
+ if other && map_entry?(other)
723
+ error(message, :name, "Expanded map entry type #{other.name} conflicts with an existing oneof type.")
724
+ end
725
+ end
726
+ end
727
+
728
+ def map_entry?(message)
729
+ message.options&.known&.[](:map_entry) == true
730
+ end
731
+
732
+ def unused_imports
733
+ @linker.table.unused.each do |name|
734
+ @linker.import_warning(name, "Import #{name} is unused.")
735
+ end
736
+ end
737
+
738
+ def style_at_least?(features, style)
739
+ order = {STYLE_LEGACY: -1, STYLE2024: 1, STYLE2026: 2}
740
+ features.enforce_naming_style != :STYLE_LEGACY &&
741
+ order.fetch(features.enforce_naming_style, 0) >= order.fetch(style)
742
+ end
743
+
744
+ def naming_style(node, kind)
745
+ return if kind == :extension_range
746
+ descriptor = schema_of(node, kind)
747
+ return unless style_at_least?(descriptor.features, :STYLE2024)
748
+ case kind
749
+ when :file
750
+ return if @file.package.empty?
751
+ problem = lower_snake_case_problem(@file.package)
752
+ error(@ast, :name, "Package name #{@file.package} #{problem}#{NAMING_OPT_OUT}") if problem
753
+ when :message, :enum, :service, :method
754
+ problem = title_case_problem(node.name)
755
+ label = {message: "Message", enum: "Enum", service: "Service", method: "Method"}.fetch(kind)
756
+ error(node, :name, "#{label} name #{node.name} #{problem}#{NAMING_OPT_OUT}") if problem
757
+ when :oneof, :field
758
+ label = (kind == :oneof) ? "Oneof" : "Field"
759
+ problem = lower_snake_case_problem(node.name)
760
+ error(node, :name, "#{label} name #{node.name} #{problem}#{NAMING_OPT_OUT}") if problem
761
+ if style_at_least?(descriptor.features, :STYLE2026) && (collision = collision_problem(node, kind))
762
+ error(node, :name, "#{label} name #{node.name} #{collision}#{COLLISION_OPT_OUT}")
763
+ end
764
+ when :enum_value
765
+ problem = upper_snake_case_problem(node.name)
766
+ error(node, :name, "Enum value name #{node.name} #{problem}#{NAMING_OPT_OUT}") if problem
767
+ end
768
+ end
769
+
770
+ def collision_problem(node, kind)
771
+ name = node.name
772
+ message = (kind == :oneof || !node.extension) ? node.parent : nil
773
+ if message
774
+ %w[has_ get_ set_ clear_].each do |prefix|
775
+ next unless name.start_with?(prefix)
776
+ rest = name.delete_prefix(prefix)
777
+ if message.field.any? { |f| f.name == rest } || message.oneof_decl.any? { |o| o.name == rest }
778
+ return "should not begin with #{prefix} if a field named #{rest} exists. This can cause collisions in " \
779
+ "generated code."
780
+ end
781
+ end
782
+ if name.end_with?("_value")
783
+ rest = name.delete_suffix("_value")
784
+ if message.field.any? { |f| f.name == rest } || message.oneof_decl.any? { |o| o.name == rest }
785
+ return "should not end with _value if a field named #{rest} exists. This can cause collisions in " \
786
+ "generated code."
787
+ end
788
+ end
789
+ end
790
+ return "should not be named descriptor. This can cause collisions in generated code." if name == "descriptor"
791
+ nil
792
+ end
793
+
794
+ def bad_underscores?(name)
795
+ return false if name.empty?
796
+ return true if name.start_with?("_") || name.end_with?("_")
797
+ (1...name.size).any? { |i| name[i - 1] == "_" && !name[i].match?(/[A-Za-z]/) }
798
+ end
799
+
800
+ def title_case_problem(name)
801
+ return "is empty" if name.empty?
802
+ return "should be TitleCase" unless name.match?(/\A[A-Za-z0-9]+\z/)
803
+ return "should begin with a capital letter" unless name.match?(/\A[A-Z]/)
804
+ nil
805
+ end
806
+
807
+ def lower_snake_case_problem(name)
808
+ return "is empty" if name.empty?
809
+ return "should be lower_snake_case" unless name.match?(/\A[a-z0-9_.]+\z/)
810
+ return "should begin with a lower case letter" unless name.match?(/\A[a-z]/)
811
+ return "contains style violating underscores" if bad_underscores?(name)
812
+ nil
813
+ end
814
+
815
+ def upper_snake_case_problem(name)
816
+ return "is empty" if name.empty?
817
+ return "should be UPPER_SNAKE_CASE" unless name.match?(/\A[A-Z0-9_]+\z/)
818
+ return "should begin with an upper case letter" unless name.match?(/\A[A-Z]/)
819
+ return "contains style violating underscores" if bad_underscores?(name)
820
+ nil
821
+ end
822
+
823
+ def proto_limits(node, kind)
824
+ return unless %i[message oneof enum].include?(kind)
825
+ descriptor = schema_of(node, kind)
826
+ return if descriptor.features.enforce_proto_limits == :LEGACY_NO_EXPLICIT_LIMITS
827
+ case kind
828
+ when :message
829
+ if descriptor.fields.size > LIMIT_FIELDS_PER_MESSAGE
830
+ error(node, :name, "Message name #{node.name} should not contain more than #{LIMIT_FIELDS_PER_MESSAGE} " \
831
+ "fields#{LIMITS_OPT_OUT}")
832
+ end
833
+ if descriptor.real_oneofs.size > LIMIT_ONEOFS_PER_MESSAGE
834
+ error(node, :name, "Message name #{node.name} should not contain more than #{LIMIT_ONEOFS_PER_MESSAGE} " \
835
+ "oneofs#{LIMITS_OPT_OUT}")
836
+ end
837
+ when :oneof
838
+ if descriptor.fields.size > LIMIT_FIELDS_PER_ONEOF
839
+ error(node, :name, "Oneof name #{node.name} should not contain more than #{LIMIT_FIELDS_PER_ONEOF} " \
840
+ "fields#{LIMITS_OPT_OUT}")
841
+ end
842
+ when :enum
843
+ if descriptor.values.size > LIMIT_VALUES_PER_ENUM
844
+ error(node, :name, "Enum name #{node.name} should not contain more than #{LIMIT_VALUES_PER_ENUM} " \
845
+ "values#{LIMITS_OPT_OUT}")
846
+ end
847
+ end
848
+ end
849
+
850
+ def symbol_visibility
851
+ strict = @file.features.default_symbol_visibility == :STRICT
852
+ each_element do |node, kind|
853
+ next if kind != :message && kind != :enum
854
+ next if node.parent.nil?
855
+ next unless node.visibility == :VISIBILITY_EXPORT
856
+ descriptor = schema_of(node, kind)
857
+ next unless descriptor.features.default_symbol_visibility == :STRICT || strict
858
+ next if kind == :enum && namespaced_enum?(descriptor)
859
+ if kind == :message
860
+ error(node, :other, "\"#{node.full_name}\" is a nested message and cannot be `export` with STRICT " \
861
+ "default_symbol_visibility. It must be moved to top-level, ideally in its own file in order to be " \
862
+ "`export`.")
863
+ else
864
+ error(node, :other, "\"#{node.full_name}\" is a nested enum and cannot be marked `export` with STRICT " \
865
+ "default_symbol_visibility. It must be moved to top-level, ideally in its own file in order to be " \
866
+ "`export`. For C++ namespacing of enums in a messages use: `local message <OuterNamespace> { export " \
867
+ "enum #{node.full_name} {...} reserved 1 to max; }`")
868
+ end
869
+ end
870
+ end
871
+
872
+ def namespaced_enum?(enum)
873
+ container = enum.containing_message
874
+ return false unless container
875
+ local_default = %i[STRICT LOCAL_ALL].include?(@file.features.default_symbol_visibility)
876
+ exported = !local_default || enum.visibility == :export
877
+ return false unless exported
878
+ return false if container.containing_message
879
+ local = container.visibility == :local || (container.visibility.nil? && local_default)
880
+ return false unless local
881
+ container.reserved_ranges.size == 1 && container.reserved_ranges[0] == (1..MAX_FIELD_NUMBER)
882
+ end
883
+
884
+ # FeatureResolver::ValidateFeatureLifetimes over every element's
885
+ # features and options
886
+ def feature_lifetimes
887
+ edition = @linker.edition_number
888
+ each_element do |node, kind|
889
+ next unless node.options
890
+ options = schema_of(node, kind).options
891
+ next if options.empty?
892
+ type = options_message(kind)
893
+ features = options[:features]
894
+ if features && (feature_type = type.fields_by_name["features"]&.message_type)
895
+ lifetimes(features, feature_type, edition, node)
896
+ end
897
+ # protoc moves features out of the options before this check
898
+ lifetimes(options, type, edition, node, except: :features)
899
+ end
900
+ end
901
+
902
+ def lifetimes(value, type, edition, node, except: nil)
903
+ fields = value.fields.filter_map do |name, v|
904
+ [type.fields_by_name[name.to_s], v] unless name == except
905
+ end
906
+ (fields + value.extensions.to_a).each do |field, v|
907
+ next unless field.is_a?(Schema::Field)
908
+ if field.type == :message || field.type == :group
909
+ (field.repeated? ? v : [v]).each do |m|
910
+ lifetimes(m, field.message_type, edition, node) if m.is_a?(Schema::MessageValue)
911
+ end
912
+ end
913
+ if field.type == :enum
914
+ (field.repeated? ? v : [v]).each do |name|
915
+ enum_value = field.enum_type.value(name.is_a?(Symbol) ? name.to_s : name)
916
+ single_lifetime(edition, enum_value.full_name, enum_value.options.feature_support, node) if enum_value
917
+ end
918
+ end
919
+ single_lifetime(edition, field.full_name, field.options.feature_support, node)
920
+ end
921
+ end
922
+
923
+ def single_lifetime(edition, full_name, support, node)
924
+ return if support.nil?
925
+ if edition < edition_value(support[:edition_introduced])
926
+ error(node, :name, "#{full_name} wasn't introduced until edition " \
927
+ "#{edition_label(support[:edition_introduced])} and can't be used in edition #{edition_label(@linker.edition)}")
928
+ end
929
+ if support.key?(:edition_removed) && edition >= edition_value(support[:edition_removed])
930
+ suffix = support.key?(:removal_error) ? ": #{support[:removal_error]}" : ""
931
+ error(node, :name, "#{full_name} has been removed in edition #{edition_label(support[:edition_removed])}#{suffix}")
932
+ elsif support.key?(:edition_deprecated) && edition >= edition_value(support[:edition_deprecated])
933
+ return unless @linker.direct?
934
+ warning(node, :name, "#{full_name} has been deprecated in edition " \
935
+ "#{edition_label(support[:edition_deprecated])}: #{support[:deprecation_warning]}")
936
+ end
937
+ end
938
+
939
+ def edition_label(edition)
940
+ edition.to_s.delete_prefix("EDITION_")
941
+ end
942
+ end
943
+ end
944
+ end