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,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protostar
|
|
4
|
+
module Lint
|
|
5
|
+
# Case conversions as buf's naming rules apply them
|
|
6
|
+
module Naming
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
# @return [Array<String>] the words of a name in any case: "HTTPServer_v2" is HTTP, Server, v2
|
|
10
|
+
def words(name)
|
|
11
|
+
name.split(/_+/).flat_map { |part| part.scan(/[A-Z]+(?![a-z])|[A-Z]?[a-z]+|[0-9]+[a-z]*|[A-Z]+/) }
|
|
12
|
+
.reject(&:empty?)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# @return [String] "fooBar" as "foo_bar"
|
|
16
|
+
def lower_snake(name)
|
|
17
|
+
joined(name).downcase
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @return [String] "fooBar" as "FOO_BAR"
|
|
21
|
+
def upper_snake(name)
|
|
22
|
+
joined(name).upcase
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @return [String] "foo_bar" as "FooBar"
|
|
26
|
+
def pascal(name)
|
|
27
|
+
words(name).map { |w| w[0].upcase + w[1..] }.join
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def lower_snake?(name)
|
|
31
|
+
name.match?(/\A[a-z][a-z0-9]*(?:_[a-z0-9]+)*\z/)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def upper_snake?(name)
|
|
35
|
+
name.match?(/\A[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)*\z/)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def pascal?(name)
|
|
39
|
+
name.match?(/\A[A-Z][A-Za-z0-9]*\z/)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Words joined by underscores, a digit run kept on the word before it
|
|
43
|
+
def joined(name)
|
|
44
|
+
out = +""
|
|
45
|
+
words(name).each do |word|
|
|
46
|
+
out << "_" unless out.empty? || word.match?(/\A[0-9]/)
|
|
47
|
+
out << word
|
|
48
|
+
end
|
|
49
|
+
out
|
|
50
|
+
end
|
|
51
|
+
private_class_method :joined
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protostar
|
|
4
|
+
module Lint
|
|
5
|
+
# The base of every lint rule. A rule is a class under {Rules} named after
|
|
6
|
+
# its id (`FIELD_NOT_REQUIRED` is `Rules::FieldNotRequired`) with two
|
|
7
|
+
# constants, `REASON` (one line) and `EXPLANATION` (why, an example, the
|
|
8
|
+
# fix), and a {#check} that calls {#report} for every problem it finds.
|
|
9
|
+
#
|
|
10
|
+
# class Rules::PackageDefined < Rule
|
|
11
|
+
# REASON = "A file without a package puts its types in the global namespace."
|
|
12
|
+
# EXPLANATION = "..."
|
|
13
|
+
#
|
|
14
|
+
# def check
|
|
15
|
+
# report(context.ast, :other, "file has no package") if context.file.package.empty?
|
|
16
|
+
# end
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# Rules read the compiled {Schema::File}, the AST positions and the source
|
|
20
|
+
# info through {Context}; none parses anything.
|
|
21
|
+
class Rule
|
|
22
|
+
# @return [String] e.g. "FIELD_NOT_REQUIRED"
|
|
23
|
+
def self.id
|
|
24
|
+
name.split("::").last.gsub(/(?<=[a-z0-9])(?=[A-Z])/, "_").upcase
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @return [String] e.g. "W0101"
|
|
28
|
+
def self.code
|
|
29
|
+
RULES.fetch(id)[0]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @return [Symbol] the smallest set the rule is in
|
|
33
|
+
def self.tier
|
|
34
|
+
RULES.fetch(id)[1]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @return [Symbol]
|
|
38
|
+
def self.category
|
|
39
|
+
RULES.fetch(id)[2]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @return [String]
|
|
43
|
+
def self.reason
|
|
44
|
+
const_get(:REASON)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @return [String]
|
|
48
|
+
def self.explanation
|
|
49
|
+
const_get(:EXPLANATION)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# The compiler codes whose warnings the rule reports as its own, so they
|
|
53
|
+
# are not printed twice
|
|
54
|
+
CLAIMS = [].freeze
|
|
55
|
+
|
|
56
|
+
# @return [Context]
|
|
57
|
+
attr_reader :context
|
|
58
|
+
# @return [Array<Compiler::Diagnostic>] what {#report} collected
|
|
59
|
+
attr_reader :diagnostics
|
|
60
|
+
|
|
61
|
+
# @param context [Context]
|
|
62
|
+
def initialize(context)
|
|
63
|
+
@context = context
|
|
64
|
+
@diagnostics = []
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Finds the rule's problems in {#context}
|
|
68
|
+
def check
|
|
69
|
+
raise NotImplementedError, "#{self.class} must define #check"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
# Records a problem at a node's recorded position
|
|
75
|
+
# @param node [Compiler::AST::Node]
|
|
76
|
+
# @param kind [Symbol] the position kind: :name, :number, :type, ...
|
|
77
|
+
# @param message [String] the headline, lower case, no full stop
|
|
78
|
+
# @param label [String, nil] under the span
|
|
79
|
+
# @param help [String, nil] the concrete fix in words
|
|
80
|
+
# @param fix [Diagnostics::Fix, nil] when the fix is mechanical and safe
|
|
81
|
+
# @param related [Array<Array(Compiler::AST::Node, Symbol, String)>] other places
|
|
82
|
+
# @param span [Diagnostics::Span, nil] instead of the node's position
|
|
83
|
+
def report(node, kind, message, label: nil, help: nil, fix: nil, related: [], span: nil)
|
|
84
|
+
span ||= @context.span(node, kind, label: label)
|
|
85
|
+
span = span.with_label(label) if span && label && span.label.nil?
|
|
86
|
+
spans = span ? [span] : []
|
|
87
|
+
related.each do |other, other_kind, other_label|
|
|
88
|
+
extra = @context.span(other, other_kind, label: other_label, primary: false)
|
|
89
|
+
spans << extra if extra
|
|
90
|
+
end
|
|
91
|
+
add(message, spans, help: help, fix: fix)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Records a problem at a span, or at none
|
|
95
|
+
def add(message, spans, help: nil, fix: nil)
|
|
96
|
+
first = spans.first
|
|
97
|
+
note = "rule #{self.class.id} (#{self.class.tier} set)"
|
|
98
|
+
@diagnostics << Compiler::Diagnostic.new(@context.name, first&.line, first&.column, message, :warning,
|
|
99
|
+
path: @context.path, spans: spans, code: self.class.code, title: message, help: [help].compact,
|
|
100
|
+
notes: [note], fix: fix, rule: self.class.id)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# @param numbers [Array<Integer>] sorted
|
|
104
|
+
# @return [String] "2, 4 to 6"
|
|
105
|
+
def numbers_text(numbers)
|
|
106
|
+
numbers.slice_when { |a, b| b != a + 1 }.map { |run| (run.size == 1) ? run[0].to_s : "#{run[0]} to #{run[-1]}" }.join(", ")
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# A rename of the identifier under a span, offered only when the old
|
|
110
|
+
# name appears once in the file (no reference, option or comment would
|
|
111
|
+
# be left behind) and the new one not at all
|
|
112
|
+
# @return [Diagnostics::Fix, nil]
|
|
113
|
+
def rename(span, from, to)
|
|
114
|
+
return nil unless span && @context.renamable?(from, to)
|
|
115
|
+
Diagnostics::Fix.new("rename `#{from}` to `#{to}`",
|
|
116
|
+
[Diagnostics::Edit.new(span.line, span.column, span.end_line, span.end_column, to)])
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protostar
|
|
4
|
+
module Lint
|
|
5
|
+
module Rules
|
|
6
|
+
# COMMENT_ENUM: A enum without a comment
|
|
7
|
+
class CommentEnum < Rule
|
|
8
|
+
REASON = "Enums are API; the comment above one is its documentation, in the .proto and in generated code."
|
|
9
|
+
EXPLANATION = <<~TEXT
|
|
10
|
+
Comments in a `.proto` are carried into generated code and API documentation. A enum without one
|
|
11
|
+
leaves every reader to guess what it means and what values it takes. A comment on the line above counts,
|
|
12
|
+
and so does one after the opening brace.
|
|
13
|
+
|
|
14
|
+
enum Status {
|
|
15
|
+
STATUS_UNSPECIFIED = 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
Fix: write what the enum is for in a `//` comment above it.
|
|
19
|
+
TEXT
|
|
20
|
+
|
|
21
|
+
def check
|
|
22
|
+
context.each(:enum) do |node, element|
|
|
23
|
+
next if context.comment(node)
|
|
24
|
+
report(node, :name, "enum `#{element.name}` has no comment", label: "undocumented",
|
|
25
|
+
help: "say what it is for in a `//` comment above it")
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protostar
|
|
4
|
+
module Lint
|
|
5
|
+
module Rules
|
|
6
|
+
# COMMENT_ENUM_VALUE: A enum value without a comment
|
|
7
|
+
class CommentEnumValue < Rule
|
|
8
|
+
REASON = "Enum values are API; the comment above one is its documentation, in the .proto and in generated code."
|
|
9
|
+
EXPLANATION = <<~TEXT
|
|
10
|
+
Comments in a `.proto` are carried into generated code and API documentation. A enum value without one
|
|
11
|
+
leaves every reader to guess what it means and what values it takes. A comment on the line above counts,
|
|
12
|
+
and so does one after the declaration on the same line.
|
|
13
|
+
|
|
14
|
+
enum Status {
|
|
15
|
+
STATUS_UNSPECIFIED = 0;
|
|
16
|
+
STATUS_ACTIVE = 1;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
Fix: write what the enum value is for in a `//` comment above it.
|
|
20
|
+
TEXT
|
|
21
|
+
|
|
22
|
+
def check
|
|
23
|
+
context.each(:enum_value) do |node, element|
|
|
24
|
+
next if context.comment(node)
|
|
25
|
+
report(node, :name, "enum value `#{element.name}` has no comment", label: "undocumented",
|
|
26
|
+
help: "say what it is for in a `//` comment above it")
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protostar
|
|
4
|
+
module Lint
|
|
5
|
+
module Rules
|
|
6
|
+
# COMMENT_FIELD: A field without a comment
|
|
7
|
+
class CommentField < Rule
|
|
8
|
+
REASON = "Fields are API; the comment above one is its documentation, in the .proto and in generated code."
|
|
9
|
+
EXPLANATION = <<~TEXT
|
|
10
|
+
Comments in a `.proto` are carried into generated code and API documentation. A field without one
|
|
11
|
+
leaves every reader to guess what it means and what values it takes. A comment on the line above counts,
|
|
12
|
+
and so does one after the declaration on the same line.
|
|
13
|
+
|
|
14
|
+
message User {
|
|
15
|
+
// Stable across renames; never reused.
|
|
16
|
+
string id = 1;
|
|
17
|
+
string name = 2;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
Fix: write what the field is for in a `//` comment above it.
|
|
21
|
+
TEXT
|
|
22
|
+
|
|
23
|
+
def check
|
|
24
|
+
context.each(:field) do |node, element|
|
|
25
|
+
next if context.comment(node)
|
|
26
|
+
report(node, :name, "field `#{element.name}` has no comment", label: "undocumented",
|
|
27
|
+
help: "say what it is for in a `//` comment above it")
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protostar
|
|
4
|
+
module Lint
|
|
5
|
+
module Rules
|
|
6
|
+
# COMMENT_MESSAGE: A message without a comment
|
|
7
|
+
class CommentMessage < Rule
|
|
8
|
+
REASON = "Messages are API; the comment above one is its documentation, in the .proto and in generated code."
|
|
9
|
+
EXPLANATION = <<~TEXT
|
|
10
|
+
Comments in a `.proto` are carried into generated code and API documentation. A message without one
|
|
11
|
+
leaves every reader to guess what it means and what values it takes. A comment on the line above counts,
|
|
12
|
+
and so does one after the opening brace.
|
|
13
|
+
|
|
14
|
+
message User {
|
|
15
|
+
string id = 1;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
Fix: write what the message is for in a `//` comment above it.
|
|
19
|
+
TEXT
|
|
20
|
+
|
|
21
|
+
def check
|
|
22
|
+
context.each(:message) do |node, element|
|
|
23
|
+
next if context.comment(node)
|
|
24
|
+
report(node, :name, "message `#{element.name}` has no comment", label: "undocumented",
|
|
25
|
+
help: "say what it is for in a `//` comment above it")
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protostar
|
|
4
|
+
module Lint
|
|
5
|
+
module Rules
|
|
6
|
+
# COMMENT_RPC: A rpc without a comment
|
|
7
|
+
class CommentRpc < Rule
|
|
8
|
+
REASON = "RPCs are API; the comment above one is its documentation, in the .proto and in generated code."
|
|
9
|
+
EXPLANATION = <<~TEXT
|
|
10
|
+
Comments in a `.proto` are carried into generated code and API documentation. A rpc without one
|
|
11
|
+
leaves every reader to guess what it means and what values it takes. A comment on the line above counts,
|
|
12
|
+
and so does one after the opening brace.
|
|
13
|
+
|
|
14
|
+
service UserService {
|
|
15
|
+
rpc GetUser(GetUserRequest) returns (GetUserResponse);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
Fix: write what the rpc is for in a `//` comment above it.
|
|
19
|
+
TEXT
|
|
20
|
+
|
|
21
|
+
def check
|
|
22
|
+
context.each(:method) do |node, element|
|
|
23
|
+
next if context.comment(node)
|
|
24
|
+
report(node, :name, "rpc `#{element.name}` has no comment", label: "undocumented",
|
|
25
|
+
help: "say what it is for in a `//` comment above it")
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protostar
|
|
4
|
+
module Lint
|
|
5
|
+
module Rules
|
|
6
|
+
# COMMENT_SERVICE: A service without a comment
|
|
7
|
+
class CommentService < Rule
|
|
8
|
+
REASON = "Services are API; the comment above one is its documentation, in the .proto and in generated code."
|
|
9
|
+
EXPLANATION = <<~TEXT
|
|
10
|
+
Comments in a `.proto` are carried into generated code and API documentation. A service without one
|
|
11
|
+
leaves every reader to guess what it means and what values it takes. A comment on the line above counts,
|
|
12
|
+
and so does one after the opening brace.
|
|
13
|
+
|
|
14
|
+
service UserService {
|
|
15
|
+
rpc GetUser(GetUserRequest) returns (GetUserResponse);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
Fix: write what the service is for in a `//` comment above it.
|
|
19
|
+
TEXT
|
|
20
|
+
|
|
21
|
+
def check
|
|
22
|
+
context.each(:service) do |node, element|
|
|
23
|
+
next if context.comment(node)
|
|
24
|
+
report(node, :name, "service `#{element.name}` has no comment", label: "undocumented",
|
|
25
|
+
help: "say what it is for in a `//` comment above it")
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protostar
|
|
4
|
+
module Lint
|
|
5
|
+
module Rules
|
|
6
|
+
# EDITION_RECOMMENDED: A proto2 or proto3 file
|
|
7
|
+
class EditionRecommended < Rule
|
|
8
|
+
REASON = "Editions replace proto2 and proto3; new features (and new defaults) only arrive there."
|
|
9
|
+
EXPLANATION = <<~TEXT
|
|
10
|
+
Editions express what `syntax` used to fix (presence, enum openness, encodings) as features that can be
|
|
11
|
+
set per file, message or field. proto2 and proto3 files keep working, but new behaviour is only added to
|
|
12
|
+
editions. A proto3 file moves to `edition = "2023"` with `option features.field_presence = IMPLICIT;`
|
|
13
|
+
to keep its semantics.
|
|
14
|
+
|
|
15
|
+
syntax = "proto3";
|
|
16
|
+
|
|
17
|
+
Fix: move to `edition = "2023";`, setting the features that keep the old semantics (see
|
|
18
|
+
https://protobuf.dev/editions/overview/).
|
|
19
|
+
TEXT
|
|
20
|
+
|
|
21
|
+
def check
|
|
22
|
+
syntax = context.file.syntax
|
|
23
|
+
return if syntax == :editions
|
|
24
|
+
add("file uses `syntax = \"#{syntax}\"` rather than an edition",
|
|
25
|
+
[context.span_at(Context::PATHS[:syntax], label: syntax.to_s)].compact,
|
|
26
|
+
help: "move to `edition = \"2023\";` with the features that keep #{syntax}'s behaviour")
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protostar
|
|
4
|
+
module Lint
|
|
5
|
+
module Rules
|
|
6
|
+
# ENUM_FIRST_VALUE_ZERO: An enum whose first value is not zero
|
|
7
|
+
class EnumFirstValueZero < Rule
|
|
8
|
+
REASON = "Readers fall back to the first value of a closed enum and to zero of an open one; both should be the \"unspecified\" value."
|
|
9
|
+
EXPLANATION = <<~TEXT
|
|
10
|
+
proto3 and open enums must start at zero, and protoc enforces it. proto2 enums are closed and may start
|
|
11
|
+
anywhere, but the first value is still the default of every field of that type, so it ends up meaning
|
|
12
|
+
"unset" whether you meant it to or not.
|
|
13
|
+
|
|
14
|
+
syntax = "proto2";
|
|
15
|
+
enum Status {
|
|
16
|
+
ACTIVE = 1;
|
|
17
|
+
SUSPENDED = 2;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
Fix: add `STATUS_UNSPECIFIED = 0;` as the first value.
|
|
21
|
+
TEXT
|
|
22
|
+
|
|
23
|
+
def check
|
|
24
|
+
context.each(:enum) do |node, enum|
|
|
25
|
+
first = enum.values.first
|
|
26
|
+
next if first.nil? || first.number.zero?
|
|
27
|
+
report(node.value[0], :number, "first value `#{first.name}` of `#{enum.name}` is not zero", label: "not 0",
|
|
28
|
+
help: "add `#{Naming.upper_snake(enum.name)}_UNSPECIFIED = 0;` as the first value")
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protostar
|
|
4
|
+
module Lint
|
|
5
|
+
module Rules
|
|
6
|
+
# ENUM_PASCAL_CASE: An enum name that is not PascalCase
|
|
7
|
+
class EnumPascalCase < Rule
|
|
8
|
+
REASON = "Enums become types in generated code; PascalCase is what generators expect."
|
|
9
|
+
EXPLANATION = <<~TEXT
|
|
10
|
+
enum order_status { ORDER_STATUS_UNSPECIFIED = 0; }
|
|
11
|
+
|
|
12
|
+
Fix: `enum OrderStatus`, and update the fields that refer to it.
|
|
13
|
+
TEXT
|
|
14
|
+
|
|
15
|
+
def check
|
|
16
|
+
context.each(:enum) do |node, enum|
|
|
17
|
+
next if Naming.pascal?(enum.name)
|
|
18
|
+
report(node, :name, "enum `#{enum.name}` is not PascalCase", label: "not PascalCase",
|
|
19
|
+
help: "name it `#{Naming.pascal(enum.name)}` and update what refers to it")
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protostar
|
|
4
|
+
module Lint
|
|
5
|
+
module Rules
|
|
6
|
+
# ENUM_VALUE_PREFIX: An enum value not prefixed with its enum's name
|
|
7
|
+
class EnumValuePrefix < Rule
|
|
8
|
+
REASON = "Enum values are scoped to the enum's parent, not the enum, so unprefixed names clash across enums."
|
|
9
|
+
EXPLANATION = <<~TEXT
|
|
10
|
+
In protobuf (as in C++) enum values are siblings of their enum: `enum Color { RED = 0; }` defines
|
|
11
|
+
`acme.RED`, and a second enum in the package cannot have a `RED`. Prefixing each value with the enum's name
|
|
12
|
+
in UPPER_SNAKE_CASE avoids that for good.
|
|
13
|
+
|
|
14
|
+
enum Color {
|
|
15
|
+
UNSPECIFIED = 0;
|
|
16
|
+
RED = 1;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
Fix: `COLOR_UNSPECIFIED = 0; COLOR_RED = 1;`. `--fix` renames a value when nothing else in the file
|
|
20
|
+
mentions it.
|
|
21
|
+
TEXT
|
|
22
|
+
|
|
23
|
+
def check
|
|
24
|
+
context.each(:enum) do |node, enum|
|
|
25
|
+
prefix = "#{Naming.upper_snake(enum.name)}_"
|
|
26
|
+
enum.values.each_with_index do |value, i|
|
|
27
|
+
next if value.name.start_with?(prefix)
|
|
28
|
+
wanted = prefix + Naming.upper_snake(value.name)
|
|
29
|
+
span = context.span(node.value[i], :name)
|
|
30
|
+
report(node.value[i], :name, "enum value `#{value.name}` is not prefixed with `#{prefix}`", span: span,
|
|
31
|
+
label: "no `#{prefix}` prefix", help: "name it `#{wanted}`", fix: rename(span, value.name, wanted))
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protostar
|
|
4
|
+
module Lint
|
|
5
|
+
module Rules
|
|
6
|
+
# ENUM_VALUE_UPPER_SNAKE_CASE: An enum value name that is not UPPER_SNAKE_CASE
|
|
7
|
+
class EnumValueUpperSnakeCase < Rule
|
|
8
|
+
REASON = "Enum values become constants; UPPER_SNAKE_CASE is the convention in every generator."
|
|
9
|
+
EXPLANATION = <<~TEXT
|
|
10
|
+
enum Color { color_unspecified = 0; colorRed = 1; }
|
|
11
|
+
|
|
12
|
+
Fix: `COLOR_UNSPECIFIED`, `COLOR_RED`. Renaming is wire-compatible but changes the JSON and text names.
|
|
13
|
+
`--fix` renames a value when nothing else in the file mentions it.
|
|
14
|
+
TEXT
|
|
15
|
+
|
|
16
|
+
def check
|
|
17
|
+
context.each(:enum_value) do |node, value|
|
|
18
|
+
next if Naming.upper_snake?(value.name)
|
|
19
|
+
wanted = Naming.upper_snake(value.name)
|
|
20
|
+
span = context.span(node, :name)
|
|
21
|
+
report(node, :name, "enum value `#{value.name}` is not UPPER_SNAKE_CASE", span: span,
|
|
22
|
+
label: "not UPPER_SNAKE_CASE", help: "name it `#{wanted}`", fix: rename(span, value.name, wanted))
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protostar
|
|
4
|
+
module Lint
|
|
5
|
+
module Rules
|
|
6
|
+
# ENUM_ZERO_VALUE_SUFFIX: An enum whose zero value does not end in `_UNSPECIFIED`
|
|
7
|
+
class EnumZeroValueSuffix < Rule
|
|
8
|
+
REASON = "The zero value is what every reader sees for an unset or unknown enum, so it should mean \"unspecified\"."
|
|
9
|
+
EXPLANATION = <<~TEXT
|
|
10
|
+
A field that was never set reads as the enum's zero value, and so does, in proto3 and open enums, a number
|
|
11
|
+
the reader does not know. If zero is a real choice (`RED`), there is no way to tell "red" from "nobody
|
|
12
|
+
said". Naming it `COLOR_UNSPECIFIED` keeps zero free to mean exactly that.
|
|
13
|
+
|
|
14
|
+
enum Color {
|
|
15
|
+
RED = 0;
|
|
16
|
+
GREEN = 1;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
Fix: add `COLOR_UNSPECIFIED = 0;` and renumber, in a new enum; in a released one, rename the zero value
|
|
20
|
+
to `COLOR_UNSPECIFIED` (the wire format does not change, the JSON and text names do). `--fix` renames it
|
|
21
|
+
when nothing else in the file mentions the old name.
|
|
22
|
+
TEXT
|
|
23
|
+
|
|
24
|
+
def check
|
|
25
|
+
context.each(:enum) do |node, enum|
|
|
26
|
+
index = enum.values.index { |v| v.number.zero? }
|
|
27
|
+
next unless index
|
|
28
|
+
value = enum.values[index]
|
|
29
|
+
next if value.name.end_with?("_UNSPECIFIED")
|
|
30
|
+
wanted = "#{Naming.upper_snake(enum.name)}_UNSPECIFIED"
|
|
31
|
+
value_node = node.value[index]
|
|
32
|
+
span = context.span(value_node, :name)
|
|
33
|
+
report(value_node, :name, "zero value `#{value.name}` of `#{enum.name}` does not end in `_UNSPECIFIED`",
|
|
34
|
+
span: span, label: "read for every unset field", help: "name it `#{wanted}`",
|
|
35
|
+
fix: rename(span, value.name, wanted))
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protostar
|
|
4
|
+
module Lint
|
|
5
|
+
module Rules
|
|
6
|
+
# FIELD_LOWER_SNAKE_CASE: A field name that is not lower_snake_case
|
|
7
|
+
class FieldLowerSnakeCase < Rule
|
|
8
|
+
REASON = "Generators derive accessors and JSON names from field names on the assumption they are lower_snake_case."
|
|
9
|
+
EXPLANATION = <<~TEXT
|
|
10
|
+
The JSON name of `foo_bar` is `fooBar`, and generated accessors camelCase or PascalCase it as each
|
|
11
|
+
language wants. A field called `fooBar` or `FooBar` makes those conversions guess, and two such fields can
|
|
12
|
+
end up with the same JSON name.
|
|
13
|
+
|
|
14
|
+
message User {
|
|
15
|
+
string firstName = 1;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
Fix: `string first_name = 1;`. Renaming is wire-compatible but changes generated accessors. `--fix`
|
|
19
|
+
renames the field when nothing else in the file mentions it.
|
|
20
|
+
TEXT
|
|
21
|
+
|
|
22
|
+
def check
|
|
23
|
+
context.each(:field) do |node, field|
|
|
24
|
+
next if node.type == :TYPE_GROUP || Naming.lower_snake?(field.name)
|
|
25
|
+
wanted = Naming.lower_snake(field.name)
|
|
26
|
+
span = context.span(node, :name)
|
|
27
|
+
report(node, :name, "field `#{field.name}` is not lower_snake_case", span: span, label: "not lower_snake_case",
|
|
28
|
+
help: "name it `#{wanted}`", fix: rename(span, field.name, wanted))
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protostar
|
|
4
|
+
module Lint
|
|
5
|
+
module Rules
|
|
6
|
+
# FIELD_NOT_REQUIRED: A proto2 `required` field, or an editions field with `LEGACY_REQUIRED` presence
|
|
7
|
+
class FieldNotRequired < Rule
|
|
8
|
+
REASON = "A required field can never be removed or made optional without breaking every reader that still checks it."
|
|
9
|
+
EXPLANATION = <<~TEXT
|
|
10
|
+
`required` looks like validation, but it is a promise about every message that will ever be written. A
|
|
11
|
+
reader built from the schema rejects a message without the field, so the field can never be removed, and
|
|
12
|
+
making it `optional` breaks every old reader that meets a message without it. Google's own style guide
|
|
13
|
+
calls required fields harmful, and proto3 dropped them.
|
|
14
|
+
|
|
15
|
+
syntax = "proto2";
|
|
16
|
+
message Order {
|
|
17
|
+
required string id = 1;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
Fix: make the field `optional` and check that it is set where the message is used. In editions, drop
|
|
21
|
+
`features.field_presence = LEGACY_REQUIRED`.
|
|
22
|
+
TEXT
|
|
23
|
+
|
|
24
|
+
def check
|
|
25
|
+
context.each(:field) do |node, field|
|
|
26
|
+
next unless field.required?
|
|
27
|
+
if node.label == :LABEL_REQUIRED
|
|
28
|
+
report(node, :name, "field `#{field.name}` is required", span: context.span_at(node.path + [4]),
|
|
29
|
+
label: "required forever", help: "make it `optional` and check that it is set in code")
|
|
30
|
+
else
|
|
31
|
+
report(node, :name, "field `#{field.name}` has LEGACY_REQUIRED presence", label: "required forever",
|
|
32
|
+
help: "remove `features.field_presence = LEGACY_REQUIRED` and check that it is set in code")
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|