protostar 0.1.0.beta1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +60 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +647 -0
  5. data/THIRD_PARTY_NOTICES.md +61 -0
  6. data/exe/protoc-gen-protostar +7 -0
  7. data/exe/protostar +7 -0
  8. data/lib/protostar/builder/generator.rb +71 -0
  9. data/lib/protostar/builder/sink.rb +35 -0
  10. data/lib/protostar/builder.rb +965 -0
  11. data/lib/protostar/cli.rb +267 -0
  12. data/lib/protostar/codec/decode_generator.rb +199 -0
  13. data/lib/protostar/codec/decoder.rb +296 -0
  14. data/lib/protostar/codec/encode_generator.rb +224 -0
  15. data/lib/protostar/codec/encoder.rb +121 -0
  16. data/lib/protostar/codec/sizer.rb +91 -0
  17. data/lib/protostar/codec.rb +15 -0
  18. data/lib/protostar/compiler/ast.rb +193 -0
  19. data/lib/protostar/compiler/emitters/descriptor_set.rb +57 -0
  20. data/lib/protostar/compiler/emitters/google_ruby.rb +103 -0
  21. data/lib/protostar/compiler/emitters/grpc.rb +175 -0
  22. data/lib/protostar/compiler/emitters/protostar_rbs.rb +252 -0
  23. data/lib/protostar/compiler/emitters/rbs.rb +394 -0
  24. data/lib/protostar/compiler/emitters/ruby.rb +503 -0
  25. data/lib/protostar/compiler/emitters.rb +268 -0
  26. data/lib/protostar/compiler/include/LICENSE +32 -0
  27. data/lib/protostar/compiler/include/google/protobuf/any.proto +106 -0
  28. data/lib/protostar/compiler/include/google/protobuf/api.proto +229 -0
  29. data/lib/protostar/compiler/include/google/protobuf/compiler/plugin.proto +180 -0
  30. data/lib/protostar/compiler/include/google/protobuf/cpp_features.proto +87 -0
  31. data/lib/protostar/compiler/include/google/protobuf/descriptor.proto +1532 -0
  32. data/lib/protostar/compiler/include/google/protobuf/duration.proto +115 -0
  33. data/lib/protostar/compiler/include/google/protobuf/empty.proto +51 -0
  34. data/lib/protostar/compiler/include/google/protobuf/field_mask.proto +243 -0
  35. data/lib/protostar/compiler/include/google/protobuf/java_features.proto +132 -0
  36. data/lib/protostar/compiler/include/google/protobuf/source_context.proto +48 -0
  37. data/lib/protostar/compiler/include/google/protobuf/struct.proto +111 -0
  38. data/lib/protostar/compiler/include/google/protobuf/timestamp.proto +145 -0
  39. data/lib/protostar/compiler/include/google/protobuf/type.proto +217 -0
  40. data/lib/protostar/compiler/include/google/protobuf/wrappers.proto +157 -0
  41. data/lib/protostar/compiler/lexer.rb +659 -0
  42. data/lib/protostar/compiler/linker.rb +894 -0
  43. data/lib/protostar/compiler/option_interpreter.rb +363 -0
  44. data/lib/protostar/compiler/parser.rb +1696 -0
  45. data/lib/protostar/compiler/retention.rb +104 -0
  46. data/lib/protostar/compiler/run.rb +184 -0
  47. data/lib/protostar/compiler/source_info.rb +161 -0
  48. data/lib/protostar/compiler/symbol_table.rb +223 -0
  49. data/lib/protostar/compiler/text_format.rb +340 -0
  50. data/lib/protostar/compiler/validator.rb +944 -0
  51. data/lib/protostar/compiler.rb +304 -0
  52. data/lib/protostar/diagnostics/codes.rb +1307 -0
  53. data/lib/protostar/diagnostics/formats.rb +64 -0
  54. data/lib/protostar/diagnostics/renderer.rb +240 -0
  55. data/lib/protostar/diagnostics/source.rb +153 -0
  56. data/lib/protostar/diagnostics.rb +117 -0
  57. data/lib/protostar/dsl.rb +367 -0
  58. data/lib/protostar/enum.rb +70 -0
  59. data/lib/protostar/errors.rb +23 -0
  60. data/lib/protostar/google/protobuf/any_pb.rb +90 -0
  61. data/lib/protostar/google/protobuf/api_pb.rb +199 -0
  62. data/lib/protostar/google/protobuf/compiler/plugin_pb.rb +168 -0
  63. data/lib/protostar/google/protobuf/descriptor_pb.rb +1239 -0
  64. data/lib/protostar/google/protobuf/duration_pb.rb +99 -0
  65. data/lib/protostar/google/protobuf/empty_pb.rb +36 -0
  66. data/lib/protostar/google/protobuf/field_mask_pb.rb +228 -0
  67. data/lib/protostar/google/protobuf/source_context_pb.rb +33 -0
  68. data/lib/protostar/google/protobuf/struct_pb.rb +91 -0
  69. data/lib/protostar/google/protobuf/timestamp_pb.rb +129 -0
  70. data/lib/protostar/google/protobuf/type_pb.rb +202 -0
  71. data/lib/protostar/google/protobuf/wrappers_pb.rb +125 -0
  72. data/lib/protostar/google_protobuf_shim.rb +24 -0
  73. data/lib/protostar/json/encoder.rb +340 -0
  74. data/lib/protostar/json/errors.rb +34 -0
  75. data/lib/protostar/json/parser.rb +437 -0
  76. data/lib/protostar/json/path.rb +39 -0
  77. data/lib/protostar/json.rb +95 -0
  78. data/lib/protostar/lint/command.rb +197 -0
  79. data/lib/protostar/lint/config.rb +98 -0
  80. data/lib/protostar/lint/context.rb +126 -0
  81. data/lib/protostar/lint/directives.rb +56 -0
  82. data/lib/protostar/lint/fixer.rb +165 -0
  83. data/lib/protostar/lint/naming.rb +54 -0
  84. data/lib/protostar/lint/rule.rb +120 -0
  85. data/lib/protostar/lint/rules/comment_enum.rb +31 -0
  86. data/lib/protostar/lint/rules/comment_enum_value.rb +32 -0
  87. data/lib/protostar/lint/rules/comment_field.rb +33 -0
  88. data/lib/protostar/lint/rules/comment_message.rb +31 -0
  89. data/lib/protostar/lint/rules/comment_rpc.rb +31 -0
  90. data/lib/protostar/lint/rules/comment_service.rb +31 -0
  91. data/lib/protostar/lint/rules/edition_recommended.rb +31 -0
  92. data/lib/protostar/lint/rules/enum_first_value_zero.rb +34 -0
  93. data/lib/protostar/lint/rules/enum_pascal_case.rb +25 -0
  94. data/lib/protostar/lint/rules/enum_value_prefix.rb +38 -0
  95. data/lib/protostar/lint/rules/enum_value_upper_snake_case.rb +28 -0
  96. data/lib/protostar/lint/rules/enum_zero_value_suffix.rb +41 -0
  97. data/lib/protostar/lint/rules/field_lower_snake_case.rb +34 -0
  98. data/lib/protostar/lint/rules/field_not_required.rb +39 -0
  99. data/lib/protostar/lint/rules/field_number_reuse_risk.rb +45 -0
  100. data/lib/protostar/lint/rules/field_numbers_hot_range.rb +44 -0
  101. data/lib/protostar/lint/rules/file_lower_snake_case.rb +27 -0
  102. data/lib/protostar/lint/rules/import_no_public.rb +29 -0
  103. data/lib/protostar/lint/rules/import_no_weak.rb +28 -0
  104. data/lib/protostar/lint/rules/import_used.rb +33 -0
  105. data/lib/protostar/lint/rules/message_pascal_case.rb +27 -0
  106. data/lib/protostar/lint/rules/no_deprecated_features.rb +50 -0
  107. data/lib/protostar/lint/rules/no_groups.rb +34 -0
  108. data/lib/protostar/lint/rules/oneof_lower_snake_case.rb +27 -0
  109. data/lib/protostar/lint/rules/package_defined.rb +28 -0
  110. data/lib/protostar/lint/rules/package_directory_match.rb +33 -0
  111. data/lib/protostar/lint/rules/package_lower_snake_case.rb +28 -0
  112. data/lib/protostar/lint/rules/package_same_directory.rb +39 -0
  113. data/lib/protostar/lint/rules/package_version_suffix.rb +31 -0
  114. data/lib/protostar/lint/rules/reserved_names_with_numbers.rb +44 -0
  115. data/lib/protostar/lint/rules/rpc_pascal_case.rb +25 -0
  116. data/lib/protostar/lint/rules/rpc_request_response_unique.rb +40 -0
  117. data/lib/protostar/lint/rules/rpc_request_standard_name.rb +32 -0
  118. data/lib/protostar/lint/rules/rpc_response_standard_name.rb +31 -0
  119. data/lib/protostar/lint/rules/service_pascal_case.rb +25 -0
  120. data/lib/protostar/lint/rules/service_suffix.rb +28 -0
  121. data/lib/protostar/lint/rules/syntax_specified.rb +35 -0
  122. data/lib/protostar/lint/runner.rb +135 -0
  123. data/lib/protostar/lint.rb +122 -0
  124. data/lib/protostar/message/accessors.rb +86 -0
  125. data/lib/protostar/message/coercion.rb +171 -0
  126. data/lib/protostar/message/field_info.rb +145 -0
  127. data/lib/protostar/message/reflection.rb +149 -0
  128. data/lib/protostar/message.rb +572 -0
  129. data/lib/protostar/plugin.rb +94 -0
  130. data/lib/protostar/schema/default_value.rb +113 -0
  131. data/lib/protostar/schema/descriptor.rb +41 -0
  132. data/lib/protostar/schema/draft/copy.rb +88 -0
  133. data/lib/protostar/schema/draft/resolution.rb +177 -0
  134. data/lib/protostar/schema/draft.rb +779 -0
  135. data/lib/protostar/schema/enum.rb +106 -0
  136. data/lib/protostar/schema/extension_range.rb +37 -0
  137. data/lib/protostar/schema/features.rb +153 -0
  138. data/lib/protostar/schema/field.rb +201 -0
  139. data/lib/protostar/schema/file.rb +217 -0
  140. data/lib/protostar/schema/freezer.rb +61 -0
  141. data/lib/protostar/schema/message.rb +104 -0
  142. data/lib/protostar/schema/oneof.rb +38 -0
  143. data/lib/protostar/schema/options.rb +367 -0
  144. data/lib/protostar/schema/pool.rb +179 -0
  145. data/lib/protostar/schema/proto.rb +576 -0
  146. data/lib/protostar/schema/resolver.rb +118 -0
  147. data/lib/protostar/schema/service.rb +89 -0
  148. data/lib/protostar/schema/source_info.rb +53 -0
  149. data/lib/protostar/schema.rb +65 -0
  150. data/lib/protostar/short_names/any.rb +7 -0
  151. data/lib/protostar/short_names/duration.rb +7 -0
  152. data/lib/protostar/short_names/empty.rb +7 -0
  153. data/lib/protostar/short_names/field_mask.rb +7 -0
  154. data/lib/protostar/short_names/struct.rb +9 -0
  155. data/lib/protostar/short_names/timestamp.rb +7 -0
  156. data/lib/protostar/short_names/wrappers.rb +15 -0
  157. data/lib/protostar/text/extensions.rb +114 -0
  158. data/lib/protostar/text/parser.rb +439 -0
  159. data/lib/protostar/text/printer.rb +304 -0
  160. data/lib/protostar/text/tokenizer.rb +116 -0
  161. data/lib/protostar/text.rb +67 -0
  162. data/lib/protostar/types.rb +152 -0
  163. data/lib/protostar/unknown_field.rb +58 -0
  164. data/lib/protostar/version.rb +5 -0
  165. data/lib/protostar/view/generator.rb +298 -0
  166. data/lib/protostar/view/map.rb +194 -0
  167. data/lib/protostar/view/repeated.rb +143 -0
  168. data/lib/protostar/view/scan.rb +183 -0
  169. data/lib/protostar/view.rb +283 -0
  170. data/lib/protostar/well_known/any.rb +52 -0
  171. data/lib/protostar/well_known/duration.rb +53 -0
  172. data/lib/protostar/well_known/list_value.rb +63 -0
  173. data/lib/protostar/well_known/struct.rb +55 -0
  174. data/lib/protostar/well_known/timestamp.rb +64 -0
  175. data/lib/protostar/well_known/value.rb +60 -0
  176. data/lib/protostar/well_known.rb +38 -0
  177. data/lib/protostar/wire/io_source.rb +40 -0
  178. data/lib/protostar/wire/reader.rb +254 -0
  179. data/lib/protostar/wire/writer.rb +201 -0
  180. data/lib/protostar/wire.rb +96 -0
  181. data/lib/protostar.rb +95 -0
  182. data/shims/google-protobuf/lib/google/protobuf/any_pb.rb +17 -0
  183. data/shims/google-protobuf/lib/google/protobuf/api_pb.rb +22 -0
  184. data/shims/google-protobuf/lib/google/protobuf/compiler/plugin_pb.rb +25 -0
  185. data/shims/google-protobuf/lib/google/protobuf/descriptor_pb.rb +72 -0
  186. data/shims/google-protobuf/lib/google/protobuf/descriptor_pool.rb +164 -0
  187. data/shims/google-protobuf/lib/google/protobuf/descriptors.rb +553 -0
  188. data/shims/google-protobuf/lib/google/protobuf/duration_pb.rb +17 -0
  189. data/shims/google-protobuf/lib/google/protobuf/empty_pb.rb +17 -0
  190. data/shims/google-protobuf/lib/google/protobuf/field_mask_pb.rb +17 -0
  191. data/shims/google-protobuf/lib/google/protobuf/internal/convert.rb +180 -0
  192. data/shims/google-protobuf/lib/google/protobuf/internal/extensions.rb +154 -0
  193. data/shims/google-protobuf/lib/google/protobuf/internal/json.rb +104 -0
  194. data/shims/google-protobuf/lib/google/protobuf/internal/well_known.rb +140 -0
  195. data/shims/google-protobuf/lib/google/protobuf/map.rb +216 -0
  196. data/shims/google-protobuf/lib/google/protobuf/message.rb +544 -0
  197. data/shims/google-protobuf/lib/google/protobuf/message_exts.rb +41 -0
  198. data/shims/google-protobuf/lib/google/protobuf/plugin_pb.rb +4 -0
  199. data/shims/google-protobuf/lib/google/protobuf/repeated_field.rb +366 -0
  200. data/shims/google-protobuf/lib/google/protobuf/source_context_pb.rb +17 -0
  201. data/shims/google-protobuf/lib/google/protobuf/struct_pb.rb +20 -0
  202. data/shims/google-protobuf/lib/google/protobuf/timestamp_pb.rb +17 -0
  203. data/shims/google-protobuf/lib/google/protobuf/type_pb.rb +27 -0
  204. data/shims/google-protobuf/lib/google/protobuf/well_known_types.rb +37 -0
  205. data/shims/google-protobuf/lib/google/protobuf/wrappers_pb.rb +25 -0
  206. data/shims/google-protobuf/lib/google/protobuf.rb +107 -0
  207. data/shims/google-protobuf/lib/google/protobuf_ffi.rb +5 -0
  208. data/shims/google-protobuf/lib/google/protobuf_native.rb +5 -0
  209. data/sig/protostar/google/protobuf/any_pb.rbs +77 -0
  210. data/sig/protostar/google/protobuf/api_pb.rbs +242 -0
  211. data/sig/protostar/google/protobuf/compiler/plugin_pb.rbs +264 -0
  212. data/sig/protostar/google/protobuf/descriptor_pb.rbs +2220 -0
  213. data/sig/protostar/google/protobuf/duration_pb.rbs +82 -0
  214. data/sig/protostar/google/protobuf/empty_pb.rbs +18 -0
  215. data/sig/protostar/google/protobuf/field_mask_pb.rbs +170 -0
  216. data/sig/protostar/google/protobuf/source_context_pb.rbs +24 -0
  217. data/sig/protostar/google/protobuf/struct_pb.rbs +135 -0
  218. data/sig/protostar/google/protobuf/timestamp_pb.rbs +102 -0
  219. data/sig/protostar/google/protobuf/type_pb.rbs +304 -0
  220. data/sig/protostar/google/protobuf/wrappers_pb.rbs +169 -0
  221. data/sig/protostar/lint.rbs +216 -0
  222. data/sig/protostar.rbs +326 -0
  223. metadata +366 -0
@@ -0,0 +1,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