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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +60 -0
- data/LICENSE.txt +21 -0
- data/README.md +647 -0
- data/THIRD_PARTY_NOTICES.md +61 -0
- data/exe/protoc-gen-protostar +7 -0
- data/exe/protostar +7 -0
- data/lib/protostar/builder/generator.rb +71 -0
- data/lib/protostar/builder/sink.rb +35 -0
- data/lib/protostar/builder.rb +965 -0
- data/lib/protostar/cli.rb +267 -0
- data/lib/protostar/codec/decode_generator.rb +199 -0
- data/lib/protostar/codec/decoder.rb +296 -0
- data/lib/protostar/codec/encode_generator.rb +224 -0
- data/lib/protostar/codec/encoder.rb +121 -0
- data/lib/protostar/codec/sizer.rb +91 -0
- data/lib/protostar/codec.rb +15 -0
- data/lib/protostar/compiler/ast.rb +193 -0
- data/lib/protostar/compiler/emitters/descriptor_set.rb +57 -0
- data/lib/protostar/compiler/emitters/google_ruby.rb +103 -0
- data/lib/protostar/compiler/emitters/grpc.rb +175 -0
- data/lib/protostar/compiler/emitters/protostar_rbs.rb +252 -0
- data/lib/protostar/compiler/emitters/rbs.rb +394 -0
- data/lib/protostar/compiler/emitters/ruby.rb +503 -0
- data/lib/protostar/compiler/emitters.rb +268 -0
- data/lib/protostar/compiler/include/LICENSE +32 -0
- data/lib/protostar/compiler/include/google/protobuf/any.proto +106 -0
- data/lib/protostar/compiler/include/google/protobuf/api.proto +229 -0
- data/lib/protostar/compiler/include/google/protobuf/compiler/plugin.proto +180 -0
- data/lib/protostar/compiler/include/google/protobuf/cpp_features.proto +87 -0
- data/lib/protostar/compiler/include/google/protobuf/descriptor.proto +1532 -0
- data/lib/protostar/compiler/include/google/protobuf/duration.proto +115 -0
- data/lib/protostar/compiler/include/google/protobuf/empty.proto +51 -0
- data/lib/protostar/compiler/include/google/protobuf/field_mask.proto +243 -0
- data/lib/protostar/compiler/include/google/protobuf/java_features.proto +132 -0
- data/lib/protostar/compiler/include/google/protobuf/source_context.proto +48 -0
- data/lib/protostar/compiler/include/google/protobuf/struct.proto +111 -0
- data/lib/protostar/compiler/include/google/protobuf/timestamp.proto +145 -0
- data/lib/protostar/compiler/include/google/protobuf/type.proto +217 -0
- data/lib/protostar/compiler/include/google/protobuf/wrappers.proto +157 -0
- data/lib/protostar/compiler/lexer.rb +659 -0
- data/lib/protostar/compiler/linker.rb +894 -0
- data/lib/protostar/compiler/option_interpreter.rb +363 -0
- data/lib/protostar/compiler/parser.rb +1696 -0
- data/lib/protostar/compiler/retention.rb +104 -0
- data/lib/protostar/compiler/run.rb +184 -0
- data/lib/protostar/compiler/source_info.rb +161 -0
- data/lib/protostar/compiler/symbol_table.rb +223 -0
- data/lib/protostar/compiler/text_format.rb +340 -0
- data/lib/protostar/compiler/validator.rb +944 -0
- data/lib/protostar/compiler.rb +304 -0
- data/lib/protostar/diagnostics/codes.rb +1307 -0
- data/lib/protostar/diagnostics/formats.rb +64 -0
- data/lib/protostar/diagnostics/renderer.rb +240 -0
- data/lib/protostar/diagnostics/source.rb +153 -0
- data/lib/protostar/diagnostics.rb +117 -0
- data/lib/protostar/dsl.rb +367 -0
- data/lib/protostar/enum.rb +70 -0
- data/lib/protostar/errors.rb +23 -0
- data/lib/protostar/google/protobuf/any_pb.rb +90 -0
- data/lib/protostar/google/protobuf/api_pb.rb +199 -0
- data/lib/protostar/google/protobuf/compiler/plugin_pb.rb +168 -0
- data/lib/protostar/google/protobuf/descriptor_pb.rb +1239 -0
- data/lib/protostar/google/protobuf/duration_pb.rb +99 -0
- data/lib/protostar/google/protobuf/empty_pb.rb +36 -0
- data/lib/protostar/google/protobuf/field_mask_pb.rb +228 -0
- data/lib/protostar/google/protobuf/source_context_pb.rb +33 -0
- data/lib/protostar/google/protobuf/struct_pb.rb +91 -0
- data/lib/protostar/google/protobuf/timestamp_pb.rb +129 -0
- data/lib/protostar/google/protobuf/type_pb.rb +202 -0
- data/lib/protostar/google/protobuf/wrappers_pb.rb +125 -0
- data/lib/protostar/google_protobuf_shim.rb +24 -0
- data/lib/protostar/json/encoder.rb +340 -0
- data/lib/protostar/json/errors.rb +34 -0
- data/lib/protostar/json/parser.rb +437 -0
- data/lib/protostar/json/path.rb +39 -0
- data/lib/protostar/json.rb +95 -0
- data/lib/protostar/lint/command.rb +197 -0
- data/lib/protostar/lint/config.rb +98 -0
- data/lib/protostar/lint/context.rb +126 -0
- data/lib/protostar/lint/directives.rb +56 -0
- data/lib/protostar/lint/fixer.rb +165 -0
- data/lib/protostar/lint/naming.rb +54 -0
- data/lib/protostar/lint/rule.rb +120 -0
- data/lib/protostar/lint/rules/comment_enum.rb +31 -0
- data/lib/protostar/lint/rules/comment_enum_value.rb +32 -0
- data/lib/protostar/lint/rules/comment_field.rb +33 -0
- data/lib/protostar/lint/rules/comment_message.rb +31 -0
- data/lib/protostar/lint/rules/comment_rpc.rb +31 -0
- data/lib/protostar/lint/rules/comment_service.rb +31 -0
- data/lib/protostar/lint/rules/edition_recommended.rb +31 -0
- data/lib/protostar/lint/rules/enum_first_value_zero.rb +34 -0
- data/lib/protostar/lint/rules/enum_pascal_case.rb +25 -0
- data/lib/protostar/lint/rules/enum_value_prefix.rb +38 -0
- data/lib/protostar/lint/rules/enum_value_upper_snake_case.rb +28 -0
- data/lib/protostar/lint/rules/enum_zero_value_suffix.rb +41 -0
- data/lib/protostar/lint/rules/field_lower_snake_case.rb +34 -0
- data/lib/protostar/lint/rules/field_not_required.rb +39 -0
- data/lib/protostar/lint/rules/field_number_reuse_risk.rb +45 -0
- data/lib/protostar/lint/rules/field_numbers_hot_range.rb +44 -0
- data/lib/protostar/lint/rules/file_lower_snake_case.rb +27 -0
- data/lib/protostar/lint/rules/import_no_public.rb +29 -0
- data/lib/protostar/lint/rules/import_no_weak.rb +28 -0
- data/lib/protostar/lint/rules/import_used.rb +33 -0
- data/lib/protostar/lint/rules/message_pascal_case.rb +27 -0
- data/lib/protostar/lint/rules/no_deprecated_features.rb +50 -0
- data/lib/protostar/lint/rules/no_groups.rb +34 -0
- data/lib/protostar/lint/rules/oneof_lower_snake_case.rb +27 -0
- data/lib/protostar/lint/rules/package_defined.rb +28 -0
- data/lib/protostar/lint/rules/package_directory_match.rb +33 -0
- data/lib/protostar/lint/rules/package_lower_snake_case.rb +28 -0
- data/lib/protostar/lint/rules/package_same_directory.rb +39 -0
- data/lib/protostar/lint/rules/package_version_suffix.rb +31 -0
- data/lib/protostar/lint/rules/reserved_names_with_numbers.rb +44 -0
- data/lib/protostar/lint/rules/rpc_pascal_case.rb +25 -0
- data/lib/protostar/lint/rules/rpc_request_response_unique.rb +40 -0
- data/lib/protostar/lint/rules/rpc_request_standard_name.rb +32 -0
- data/lib/protostar/lint/rules/rpc_response_standard_name.rb +31 -0
- data/lib/protostar/lint/rules/service_pascal_case.rb +25 -0
- data/lib/protostar/lint/rules/service_suffix.rb +28 -0
- data/lib/protostar/lint/rules/syntax_specified.rb +35 -0
- data/lib/protostar/lint/runner.rb +135 -0
- data/lib/protostar/lint.rb +122 -0
- data/lib/protostar/message/accessors.rb +86 -0
- data/lib/protostar/message/coercion.rb +171 -0
- data/lib/protostar/message/field_info.rb +145 -0
- data/lib/protostar/message/reflection.rb +149 -0
- data/lib/protostar/message.rb +572 -0
- data/lib/protostar/plugin.rb +94 -0
- data/lib/protostar/schema/default_value.rb +113 -0
- data/lib/protostar/schema/descriptor.rb +41 -0
- data/lib/protostar/schema/draft/copy.rb +88 -0
- data/lib/protostar/schema/draft/resolution.rb +177 -0
- data/lib/protostar/schema/draft.rb +779 -0
- data/lib/protostar/schema/enum.rb +106 -0
- data/lib/protostar/schema/extension_range.rb +37 -0
- data/lib/protostar/schema/features.rb +153 -0
- data/lib/protostar/schema/field.rb +201 -0
- data/lib/protostar/schema/file.rb +217 -0
- data/lib/protostar/schema/freezer.rb +61 -0
- data/lib/protostar/schema/message.rb +104 -0
- data/lib/protostar/schema/oneof.rb +38 -0
- data/lib/protostar/schema/options.rb +367 -0
- data/lib/protostar/schema/pool.rb +179 -0
- data/lib/protostar/schema/proto.rb +576 -0
- data/lib/protostar/schema/resolver.rb +118 -0
- data/lib/protostar/schema/service.rb +89 -0
- data/lib/protostar/schema/source_info.rb +53 -0
- data/lib/protostar/schema.rb +65 -0
- data/lib/protostar/short_names/any.rb +7 -0
- data/lib/protostar/short_names/duration.rb +7 -0
- data/lib/protostar/short_names/empty.rb +7 -0
- data/lib/protostar/short_names/field_mask.rb +7 -0
- data/lib/protostar/short_names/struct.rb +9 -0
- data/lib/protostar/short_names/timestamp.rb +7 -0
- data/lib/protostar/short_names/wrappers.rb +15 -0
- data/lib/protostar/text/extensions.rb +114 -0
- data/lib/protostar/text/parser.rb +439 -0
- data/lib/protostar/text/printer.rb +304 -0
- data/lib/protostar/text/tokenizer.rb +116 -0
- data/lib/protostar/text.rb +67 -0
- data/lib/protostar/types.rb +152 -0
- data/lib/protostar/unknown_field.rb +58 -0
- data/lib/protostar/version.rb +5 -0
- data/lib/protostar/view/generator.rb +298 -0
- data/lib/protostar/view/map.rb +194 -0
- data/lib/protostar/view/repeated.rb +143 -0
- data/lib/protostar/view/scan.rb +183 -0
- data/lib/protostar/view.rb +283 -0
- data/lib/protostar/well_known/any.rb +52 -0
- data/lib/protostar/well_known/duration.rb +53 -0
- data/lib/protostar/well_known/list_value.rb +63 -0
- data/lib/protostar/well_known/struct.rb +55 -0
- data/lib/protostar/well_known/timestamp.rb +64 -0
- data/lib/protostar/well_known/value.rb +60 -0
- data/lib/protostar/well_known.rb +38 -0
- data/lib/protostar/wire/io_source.rb +40 -0
- data/lib/protostar/wire/reader.rb +254 -0
- data/lib/protostar/wire/writer.rb +201 -0
- data/lib/protostar/wire.rb +96 -0
- data/lib/protostar.rb +95 -0
- data/shims/google-protobuf/lib/google/protobuf/any_pb.rb +17 -0
- data/shims/google-protobuf/lib/google/protobuf/api_pb.rb +22 -0
- data/shims/google-protobuf/lib/google/protobuf/compiler/plugin_pb.rb +25 -0
- data/shims/google-protobuf/lib/google/protobuf/descriptor_pb.rb +72 -0
- data/shims/google-protobuf/lib/google/protobuf/descriptor_pool.rb +164 -0
- data/shims/google-protobuf/lib/google/protobuf/descriptors.rb +553 -0
- data/shims/google-protobuf/lib/google/protobuf/duration_pb.rb +17 -0
- data/shims/google-protobuf/lib/google/protobuf/empty_pb.rb +17 -0
- data/shims/google-protobuf/lib/google/protobuf/field_mask_pb.rb +17 -0
- data/shims/google-protobuf/lib/google/protobuf/internal/convert.rb +180 -0
- data/shims/google-protobuf/lib/google/protobuf/internal/extensions.rb +154 -0
- data/shims/google-protobuf/lib/google/protobuf/internal/json.rb +104 -0
- data/shims/google-protobuf/lib/google/protobuf/internal/well_known.rb +140 -0
- data/shims/google-protobuf/lib/google/protobuf/map.rb +216 -0
- data/shims/google-protobuf/lib/google/protobuf/message.rb +544 -0
- data/shims/google-protobuf/lib/google/protobuf/message_exts.rb +41 -0
- data/shims/google-protobuf/lib/google/protobuf/plugin_pb.rb +4 -0
- data/shims/google-protobuf/lib/google/protobuf/repeated_field.rb +366 -0
- data/shims/google-protobuf/lib/google/protobuf/source_context_pb.rb +17 -0
- data/shims/google-protobuf/lib/google/protobuf/struct_pb.rb +20 -0
- data/shims/google-protobuf/lib/google/protobuf/timestamp_pb.rb +17 -0
- data/shims/google-protobuf/lib/google/protobuf/type_pb.rb +27 -0
- data/shims/google-protobuf/lib/google/protobuf/well_known_types.rb +37 -0
- data/shims/google-protobuf/lib/google/protobuf/wrappers_pb.rb +25 -0
- data/shims/google-protobuf/lib/google/protobuf.rb +107 -0
- data/shims/google-protobuf/lib/google/protobuf_ffi.rb +5 -0
- data/shims/google-protobuf/lib/google/protobuf_native.rb +5 -0
- data/sig/protostar/google/protobuf/any_pb.rbs +77 -0
- data/sig/protostar/google/protobuf/api_pb.rbs +242 -0
- data/sig/protostar/google/protobuf/compiler/plugin_pb.rbs +264 -0
- data/sig/protostar/google/protobuf/descriptor_pb.rbs +2220 -0
- data/sig/protostar/google/protobuf/duration_pb.rbs +82 -0
- data/sig/protostar/google/protobuf/empty_pb.rbs +18 -0
- data/sig/protostar/google/protobuf/field_mask_pb.rbs +170 -0
- data/sig/protostar/google/protobuf/source_context_pb.rbs +24 -0
- data/sig/protostar/google/protobuf/struct_pb.rbs +135 -0
- data/sig/protostar/google/protobuf/timestamp_pb.rbs +102 -0
- data/sig/protostar/google/protobuf/type_pb.rbs +304 -0
- data/sig/protostar/google/protobuf/wrappers_pb.rbs +169 -0
- data/sig/protostar/lint.rbs +216 -0
- data/sig/protostar.rbs +326 -0
- 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
|