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,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Protostar
4
+ module Lint
5
+ module Rules
6
+ # RPC_RESPONSE_STANDARD_NAME: An rpc whose response is not `<Rpc>Response`
7
+ class RpcResponseStandardName < Rule
8
+ REASON = "A response message of its own per RPC can grow fields without touching any other RPC."
9
+ EXPLANATION = <<~TEXT
10
+ Returning a shared type such as `User` means a field added for one RPC shows up in all of them.
11
+
12
+ rpc GetUser(GetUserRequest) returns (User);
13
+
14
+ Fix: `returns (GetUserResponse)`, with a `User user = 1;` field in it.
15
+ TEXT
16
+
17
+ def check
18
+ context.each(:service) do |service_node, service|
19
+ service.rpcs.each_with_index do |rpc, i|
20
+ base = Naming.pascal(rpc.name)
21
+ wanted = ["#{base}Response", "#{service.name}#{base}Response"]
22
+ next if wanted.include?(rpc.output_type.name)
23
+ report(service_node.rpcs[i], :output_type, "response of `#{rpc.name}` is `#{rpc.output_type.name}`, not `#{wanted[0]}`",
24
+ label: "not `#{wanted[0]}`", help: "return a `#{wanted[0]}` message of its own")
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Protostar
4
+ module Lint
5
+ module Rules
6
+ # SERVICE_PASCAL_CASE: A service name that is not PascalCase
7
+ class ServicePascalCase < Rule
8
+ REASON = "Services become classes and interfaces in generated code; PascalCase is what generators expect."
9
+ EXPLANATION = <<~TEXT
10
+ service user_api {}
11
+
12
+ Fix: `service UserAPIService {}`.
13
+ TEXT
14
+
15
+ def check
16
+ context.each(:service) do |node, service|
17
+ next if Naming.pascal?(service.name)
18
+ report(node, :name, "service `#{service.name}` is not PascalCase", label: "not PascalCase",
19
+ help: "name it `#{Naming.pascal(service.name)}`")
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Protostar
4
+ module Lint
5
+ module Rules
6
+ # SERVICE_SUFFIX: A service whose name does not end in `Service`
7
+ class ServiceSuffix < Rule
8
+ REASON = "A `Service` suffix keeps the generated client and server names from colliding with message names."
9
+ EXPLANATION = <<~TEXT
10
+ Generators name stubs and base classes after the service; `service User` next to `message User` makes
11
+ those collide or read ambiguously.
12
+
13
+ service Users { rpc Get(GetRequest) returns (GetResponse); }
14
+
15
+ Fix: `service UsersService`.
16
+ TEXT
17
+
18
+ def check
19
+ context.each(:service) do |node, service|
20
+ next if service.name.end_with?("Service")
21
+ report(node, :name, "service `#{service.name}` does not end in `Service`", label: "no `Service` suffix",
22
+ help: "name it `#{service.name}Service`")
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Protostar
4
+ module Lint
5
+ module Rules
6
+ # SYNTAX_SPECIFIED: A file without a `syntax` or `edition` line
7
+ class SyntaxSpecified < Rule
8
+ REASON = "Without `syntax` or `edition` a file is proto2, which is rarely what its author meant."
9
+ EXPLANATION = <<~TEXT
10
+ protoc reads a file with neither line as proto2, silently. Fields then need labels, enums are closed, and
11
+ defaults behave differently from proto3; a reader of the file cannot tell which was intended.
12
+
13
+ message User {
14
+ optional string name = 1;
15
+ }
16
+
17
+ Fix: start the file with `edition = "2023";` (or `syntax = "proto3";`). `--fix` adds
18
+ `syntax = "proto2";`, which spells out what the file already is.
19
+ TEXT
20
+
21
+ def check
22
+ return if context.ast.syntax || context.ast.edition
23
+ text = context.text
24
+ fix = unless text.start_with?("\xEF\xBB\xBF".b) || text.b.start_with?("\xEF\xBB\xBF".b)
25
+ Diagnostics::Fix.new("add `syntax = \"proto2\";`, which is what the file already is",
26
+ [Diagnostics::Edit.new(1, 1, 1, 1, "syntax = \"proto2\";\n")])
27
+ end
28
+ span = Diagnostics::Span.new(1, 1, label: "proto2 by default")
29
+ add("file `#{context.name}` has no `syntax` or `edition` line", [span],
30
+ help: "start the file with `edition = \"2023\";` or `syntax = \"proto3\";`", fix: fix)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Protostar
4
+ module Lint
5
+ # Compiles each file on its own, so one that fails does not stop the
6
+ # others, sharing one pool so imports are built once; then runs the rules
7
+ # over every file that compiled.
8
+ #
9
+ # The compiler's diagnostics come first, in the order it reports them
10
+ # (protoc's): errors from any file, warnings for the files asked about.
11
+ # Then each file's lint findings, in file order and by position within a
12
+ # file. Comment directives switch off warnings, never errors.
13
+ class Runner
14
+ # What a run found
15
+ class Result
16
+ # @return [Array<Compiler::Diagnostic>]
17
+ attr_reader :diagnostics
18
+ # @return [Array<Context>] the files that compiled
19
+ attr_reader :contexts
20
+ # @return [Array<String>] every file asked about
21
+ attr_reader :names
22
+
23
+ def initialize(diagnostics, contexts, names)
24
+ @diagnostics = diagnostics.freeze
25
+ @contexts = contexts.freeze
26
+ @names = names.freeze
27
+ freeze
28
+ end
29
+
30
+ def errors?
31
+ @diagnostics.any?(&:error?)
32
+ end
33
+
34
+ def warnings?
35
+ @diagnostics.any?(&:warning?)
36
+ end
37
+ end
38
+
39
+ # @return [Compiler]
40
+ attr_reader :compiler
41
+
42
+ # @param include_paths [Array<String>] where file names and imports are resolved
43
+ # @param rules [Array<String>] rule ids to run, from {Lint.select}
44
+ # @param except [Array<String>] rule ids switched off by name; the compiler
45
+ # warnings they would report are not passed through either
46
+ # @param directives [Boolean] honour `// protostar-lint:` comments
47
+ def initialize(include_paths:, rules:, except: [], directives: true)
48
+ @compiler = Compiler.new(include_paths: include_paths)
49
+ @rules = rules.map { |id| Lint.rule_class(id) }
50
+ @claimed = (rules + except).flat_map { |id| Lint.rule_class(id)::CLAIMS }.uniq
51
+ @directives = directives
52
+ @texts = {}
53
+ end
54
+
55
+ # @param names [Array<String>] file names relative to an include path
56
+ # @param sources [Hash{String => String}] file contents by name, before the include paths
57
+ # @return [Result]
58
+ def run(names, sources: {})
59
+ @sources = sources
60
+ reported = []
61
+ seen = {}
62
+ linkers = {}
63
+ names.each do |name|
64
+ run = Compiler::Run.new(@compiler, [name], true, sources: sources, all_direct: true)
65
+ if linkers[name]
66
+ # built as an import of an earlier file: only the checks protoc
67
+ # runs on the files it is given are left
68
+ Compiler::Validator.cli_checks([linkers[name]], run)
69
+ else
70
+ run.call
71
+ end
72
+ run.diagnostics.each do |d|
73
+ next if seen[d]
74
+ seen[d] = true
75
+ reported << d
76
+ end
77
+ names.each { |n| linkers[n] ||= run.linker(n) }
78
+ end
79
+ failed = reported.select(&:error?).map(&:file).uniq
80
+ contexts = []
81
+ names.each do |name|
82
+ next if linkers[name].nil? || failed.include?(name)
83
+ warnings = reported.select { |d| d.warning? && d.file == name }
84
+ contexts << Context.new(name: name, path: shown_path(name), disk_path: disk_path(name),
85
+ linker: linkers[name], text: text(name), warnings: warnings, all: contexts)
86
+ end
87
+ diagnostics = reported.select do |d|
88
+ d.error? || (names.include?(d.file) && !@claimed.include?(d.code) && !suppressed?(d))
89
+ end
90
+ contexts.each { |context| diagnostics.concat(lint(context)) }
91
+ Result.new(diagnostics, contexts, names)
92
+ end
93
+
94
+ # @param name [String]
95
+ # @return [String, nil] the file's text, from the sources given or from disk
96
+ def text(name)
97
+ return @texts[name] if @texts.key?(name)
98
+ @texts[name] = if @sources&.key?(name)
99
+ @sources[name]
100
+ elsif (path = @compiler.resolve(name))
101
+ ::File.binread(path)
102
+ end
103
+ end
104
+
105
+ private
106
+
107
+ def lint(context)
108
+ found = @rules.flat_map do |rule_class|
109
+ rule = rule_class.new(context)
110
+ rule.check
111
+ rule.diagnostics
112
+ end
113
+ found.reject! { |d| suppressed?(d) }
114
+ found.each_with_index.sort_by { |d, i| [d.line || 0, d.column || 0, i] }.map(&:first)
115
+ end
116
+
117
+ def suppressed?(diagnostic)
118
+ return false unless @directives && diagnostic.warning?
119
+ @parsed ||= {}
120
+ directives = @parsed[diagnostic.file] ||= (text = text(diagnostic.file)) && Directives.new(text)
121
+ directives ? directives.suppressed?(diagnostic) : false
122
+ end
123
+
124
+ def shown_path(name)
125
+ return nil if @sources.key?(name)
126
+ @compiler.disk_path(name)
127
+ end
128
+
129
+ def disk_path(name)
130
+ return nil if @sources.key?(name)
131
+ @compiler.resolve(name)
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Protostar
4
+ # `protostar lint`: compiles `.proto` files, reports the compiler's errors
5
+ # with {Diagnostics::Renderer}, and checks the files that compiled against
6
+ # best-practice rules, the kind `buf lint` checks, under the same names.
7
+ #
8
+ # runner = Protostar::Lint::Runner.new(include_paths: ["proto"], rules: Protostar::Lint.select(["default"]))
9
+ # result = runner.run(["acme/person.proto"])
10
+ # result.diagnostics.each { |d| puts d }
11
+ #
12
+ # Each rule is one class under {Lint::Rules}, in a file named after its id,
13
+ # and one line in {RULES}.
14
+ module Lint
15
+ autoload :Rule, ::File.expand_path("lint/rule", __dir__)
16
+ autoload :Context, ::File.expand_path("lint/context", __dir__)
17
+ autoload :Naming, ::File.expand_path("lint/naming", __dir__)
18
+ autoload :Config, ::File.expand_path("lint/config", __dir__)
19
+ autoload :Directives, ::File.expand_path("lint/directives", __dir__)
20
+ autoload :Runner, ::File.expand_path("lint/runner", __dir__)
21
+ autoload :Fixer, ::File.expand_path("lint/fixer", __dir__)
22
+ autoload :Command, ::File.expand_path("lint/command", __dir__)
23
+
24
+ # Raised for a configuration or command-line mistake
25
+ class Error < Protostar::Error; end
26
+
27
+ # The sets a rule can be in, each including the one before
28
+ TIERS = %i[minimal default strict].freeze
29
+
30
+ # Every rule: id => [code, the smallest set it is in, its category]
31
+ RULES = {
32
+ "FIELD_NOT_REQUIRED" => ["W0101", :minimal, :longevity],
33
+ "NO_GROUPS" => ["W0102", :minimal, :longevity],
34
+ "ENUM_ZERO_VALUE_SUFFIX" => ["W0103", :minimal, :longevity],
35
+ "ENUM_FIRST_VALUE_ZERO" => ["W0104", :minimal, :longevity],
36
+ "FIELD_NUMBER_REUSE_RISK" => ["W0105", :minimal, :longevity],
37
+ "RESERVED_NAMES_WITH_NUMBERS" => ["W0106", :minimal, :longevity],
38
+ "PACKAGE_DEFINED" => ["W0107", :minimal, :longevity],
39
+ "IMPORT_NO_PUBLIC" => ["W0108", :minimal, :longevity],
40
+ "IMPORT_NO_WEAK" => ["W0109", :minimal, :longevity],
41
+ "IMPORT_USED" => ["W0110", :minimal, :longevity],
42
+ "SYNTAX_SPECIFIED" => ["W0111", :minimal, :longevity],
43
+ "FILE_LOWER_SNAKE_CASE" => ["W0201", :default, :naming],
44
+ "PACKAGE_LOWER_SNAKE_CASE" => ["W0202", :default, :naming],
45
+ "MESSAGE_PASCAL_CASE" => ["W0203", :default, :naming],
46
+ "FIELD_LOWER_SNAKE_CASE" => ["W0204", :default, :naming],
47
+ "ONEOF_LOWER_SNAKE_CASE" => ["W0205", :default, :naming],
48
+ "ENUM_PASCAL_CASE" => ["W0206", :default, :naming],
49
+ # before UPPER_SNAKE_CASE: when both rename a value, the prefixed name fixes both
50
+ "ENUM_VALUE_PREFIX" => ["W0208", :default, :naming],
51
+ "ENUM_VALUE_UPPER_SNAKE_CASE" => ["W0207", :default, :naming],
52
+ "SERVICE_PASCAL_CASE" => ["W0209", :default, :naming],
53
+ "SERVICE_SUFFIX" => ["W0210", :default, :naming],
54
+ "RPC_PASCAL_CASE" => ["W0211", :default, :naming],
55
+ "RPC_REQUEST_STANDARD_NAME" => ["W0212", :default, :naming],
56
+ "RPC_RESPONSE_STANDARD_NAME" => ["W0213", :default, :naming],
57
+ "RPC_REQUEST_RESPONSE_UNIQUE" => ["W0214", :default, :naming],
58
+ "PACKAGE_DIRECTORY_MATCH" => ["W0301", :default, :structure],
59
+ "PACKAGE_SAME_DIRECTORY" => ["W0302", :default, :structure],
60
+ "NO_DEPRECATED_FEATURES" => ["W0303", :default, :structure],
61
+ "PACKAGE_VERSION_SUFFIX" => ["W0304", :strict, :structure],
62
+ "FIELD_NUMBERS_HOT_RANGE" => ["W0305", :strict, :structure],
63
+ "COMMENT_MESSAGE" => ["W0401", :strict, :documentation],
64
+ "COMMENT_FIELD" => ["W0402", :strict, :documentation],
65
+ "COMMENT_ENUM" => ["W0403", :strict, :documentation],
66
+ "COMMENT_ENUM_VALUE" => ["W0404", :strict, :documentation],
67
+ "COMMENT_SERVICE" => ["W0405", :strict, :documentation],
68
+ "COMMENT_RPC" => ["W0406", :strict, :documentation],
69
+ "EDITION_RECOMMENDED" => ["W0501", :strict, :editions]
70
+ }.freeze
71
+
72
+ # The rule classes, each loading its file on first use
73
+ module Rules
74
+ RULES.each_key do |id|
75
+ autoload(id.split("_").map(&:capitalize).join.to_sym, ::File.expand_path("lint/rules/#{id.downcase}", __dir__))
76
+ end
77
+ end
78
+
79
+ # @return [Array<Symbol>] the category names a rule list can use
80
+ def self.categories
81
+ RULES.values.map(&:last).uniq
82
+ end
83
+
84
+ # @param id [String] a rule id
85
+ # @return [Class] its {Rule} subclass
86
+ def self.rule_class(id)
87
+ Rules.const_get(id.split("_").map(&:capitalize).join, false)
88
+ end
89
+
90
+ # @param id_or_code [String] "FIELD_NOT_REQUIRED" or "W0101"
91
+ # @return [String, nil] the rule id
92
+ def self.rule_id(id_or_code)
93
+ key = id_or_code.upcase
94
+ return key if RULES.key?(key)
95
+ RULES.find { |_, (code, _, _)| code == key }&.first
96
+ end
97
+
98
+ # The rules a list of set names, category names and rule ids selects, in
99
+ # {RULES} order
100
+ # @param names [Array<String>] e.g. ["default", "documentation", "PACKAGE_VERSION_SUFFIX"]
101
+ # @param except [Array<String>] rule ids or codes to leave out
102
+ # @return [Array<String>] rule ids
103
+ # @raise [Error] for a name that is none of these
104
+ def self.select(names, except: [])
105
+ chosen = names.flat_map do |name|
106
+ key = name.to_s.strip
107
+ if TIERS.include?(key.downcase.to_sym)
108
+ upto = TIERS.index(key.downcase.to_sym)
109
+ RULES.select { |_, (_, tier, _)| TIERS.index(tier) <= upto }.keys
110
+ elsif categories.include?(key.downcase.to_sym)
111
+ RULES.select { |_, (_, _, category)| category == key.downcase.to_sym }.keys
112
+ elsif (id = rule_id(key))
113
+ [id]
114
+ else
115
+ raise Error, "Unknown rule or set: #{key} (sets: #{TIERS.join(", ")}; categories: #{categories.join(", ")})"
116
+ end
117
+ end
118
+ skipped = except.map { |name| rule_id(name.to_s.strip) || raise(Error, "Unknown rule: #{name}") }
119
+ RULES.keys & (chosen - skipped)
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Protostar
4
+ class Message
5
+ # Writes the field accessors of a message class as Ruby source and evals
6
+ # it into the class. Methods made from source, unlike `define_method`
7
+ # blocks, can be called from any Ractor, and they read their slot by a
8
+ # literal index.
9
+ # @api private
10
+ module Accessors
11
+ module_function
12
+
13
+ # Methods every message has; a field whose accessor would replace one
14
+ # is reachable only through `[]` and `[]=`
15
+ def reserved
16
+ (Message.public_instance_methods + %i[initialize]).to_h { |m| [m, true] }
17
+ end
18
+
19
+ # @param klass [Class]
20
+ # @param fields [Array<FieldInfo>]
21
+ # @param oneofs [Array<Array(Symbol, Array<Integer>)>] real oneofs: name and member slots
22
+ # @param reserved [Hash{Symbol => true}] from {reserved}, computed once for many classes
23
+ def install(klass, fields, oneofs, reserved = self.reserved)
24
+ taken = reserved.dup
25
+ src = +""
26
+ fields.each do |info|
27
+ name = info.name
28
+ i = info.index
29
+ reader = name.to_sym
30
+ writer = :"#{name}="
31
+ unless taken[reader]
32
+ taken[reader] = true
33
+ src << reader_source(name, info)
34
+ end
35
+ unless taken[writer]
36
+ taken[writer] = true
37
+ src << "def #{name}=(v); __set(#{i}, v); end\n"
38
+ end
39
+ next unless info.presence
40
+ has = :"has_#{name}?"
41
+ clear = :"clear_#{name}"
42
+ unless taken[has]
43
+ taken[has] = true
44
+ src << "def has_#{name}?; !@values[#{i}].nil?; end\n"
45
+ end
46
+ unless taken[clear]
47
+ taken[clear] = true
48
+ src << "def clear_#{name}; __clear(#{i}); end\n"
49
+ end
50
+ end
51
+ oneofs.each do |oneof_name, slots|
52
+ which = :"which_#{oneof_name}"
53
+ next if taken[which]
54
+ taken[which] = true
55
+ src << "def which_#{oneof_name}\n"
56
+ slots.each do |slot|
57
+ src << " return :#{fields[slot].name} unless @values[#{slot}].nil?\n"
58
+ end
59
+ src << " nil\nend\n"
60
+ end
61
+ klass.class_eval(src, "(protostar #{klass.descriptor.full_name})", 1)
62
+ end
63
+
64
+ def reader_source(name, info)
65
+ i = info.index
66
+ if info.repeated || info.map
67
+ "def #{name}; @values[#{i}] || __container(#{i}); end\n"
68
+ elsif info.message? || !info.presence
69
+ "def #{name}; @values[#{i}]; end\n"
70
+ else
71
+ "def #{name}; v = @values[#{i}]; v.nil? ? #{default_literal(info)} : v; end\n"
72
+ end
73
+ end
74
+
75
+ # The default as a literal when it has an exact one, otherwise a lookup
76
+ def default_literal(info)
77
+ d = info.default
78
+ case d
79
+ when Integer, true, false, Symbol then d.inspect
80
+ when Float then d.finite? ? d.inspect : "self.class.__fields[#{info.index}].default"
81
+ else "self.class.__fields[#{info.index}].default"
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Protostar
4
+ class Message
5
+ # Checks and converts values assigned to fields. Everything stored in a
6
+ # message has been through here (or through the decoder, which produces
7
+ # the same shapes): Integers in range, Floats, true/false, frozen UTF-8 or
8
+ # binary Strings, enum Symbols (or Integers for numbers without a name),
9
+ # messages of the field's class.
10
+ # @api private
11
+ module Coercion
12
+ RANGES = {
13
+ FieldInfo::INT32 => -0x8000_0000..0x7FFF_FFFF,
14
+ FieldInfo::SINT32 => -0x8000_0000..0x7FFF_FFFF,
15
+ FieldInfo::SFIXED32 => -0x8000_0000..0x7FFF_FFFF,
16
+ FieldInfo::UINT32 => 0..0xFFFF_FFFF,
17
+ FieldInfo::FIXED32 => 0..0xFFFF_FFFF,
18
+ FieldInfo::INT64 => -0x8000_0000_0000_0000..0x7FFF_FFFF_FFFF_FFFF,
19
+ FieldInfo::SINT64 => -0x8000_0000_0000_0000..0x7FFF_FFFF_FFFF_FFFF,
20
+ FieldInfo::SFIXED64 => -0x8000_0000_0000_0000..0x7FFF_FFFF_FFFF_FFFF,
21
+ FieldInfo::UINT64 => 0..0xFFFF_FFFF_FFFF_FFFF,
22
+ FieldInfo::FIXED64 => 0..0xFFFF_FFFF_FFFF_FFFF
23
+ }.freeze
24
+
25
+ ENUM_RANGE = RANGES.fetch(FieldInfo::INT32)
26
+ FLOAT_MAX = 3.4028234663852886e+38
27
+ # Halfway between FLT_MAX and 2**128: doubles below it round to FLT_MAX
28
+ FLOAT_ROUNDS_TO_MAX = 3.4028235677973366e+38
29
+ UTF_8 = Encoding::UTF_8
30
+ BINARY = Encoding::BINARY
31
+
32
+ module_function
33
+
34
+ # @param info [FieldInfo]
35
+ # @return [Object] what gets stored for a non-nil value assigned to the field
36
+ # @raise [Protostar::TypeError]
37
+ def field(info, value)
38
+ if info.map
39
+ raise_type(info, value, "a Hash") unless value.is_a?(Hash)
40
+ key = info.key
41
+ val = info.value
42
+ out = {}
43
+ value.each { |k, v| out[scalar(key, k)] = single(val, v) }
44
+ out
45
+ elsif info.repeated
46
+ raise_type(info, value, "an Array") unless value.is_a?(Array)
47
+ value.map { |v| single(info, v) }
48
+ else
49
+ single(info, value)
50
+ end
51
+ end
52
+
53
+ # One value of a singular field, an element of a repeated field, or a map value
54
+ def single(info, value)
55
+ if info.message?
56
+ klass = info.message_class
57
+ return value if value.instance_of?(klass)
58
+ return klass.new(value) if value.is_a?(Hash)
59
+ begin
60
+ coerced = klass.__coerce(value)
61
+ rescue RangeError
62
+ raise_type(info, value, "a finite value in #{klass.descriptor.full_name}'s range")
63
+ end
64
+ return coerced if coerced
65
+ raise_type(info, value, "a #{klass.descriptor.full_name} or a Hash")
66
+ end
67
+ scalar(info, value)
68
+ end
69
+
70
+ def scalar(info, value)
71
+ case (kind = info.kind)
72
+ when FieldInfo::DOUBLE
73
+ raise_type(info, value, "a Numeric") unless value.is_a?(Float) || value.is_a?(Integer)
74
+ value.to_f
75
+ when FieldInfo::FLOAT
76
+ raise_type(info, value, "a Numeric") unless value.is_a?(Float) || value.is_a?(Integer)
77
+ # stored at single precision so that reads match what decoding gives back
78
+ float32(value.to_f)
79
+ when FieldInfo::BOOL
80
+ raise_type(info, value, "true or false") unless value == true || value == false
81
+ value
82
+ when FieldInfo::STRING
83
+ string(info, value)
84
+ when FieldInfo::BYTES
85
+ raise_type(info, value, "a String") unless value.is_a?(String)
86
+ (value.frozen? && value.encoding == BINARY) ? value : value.b.freeze
87
+ when FieldInfo::ENUM
88
+ enum(info, value)
89
+ else
90
+ range = RANGES.fetch(kind)
91
+ unless value.is_a?(Integer) && range.cover?(value)
92
+ raise_type(info, value, "an Integer in #{info.descriptor.type} range")
93
+ end
94
+ value
95
+ end
96
+ end
97
+
98
+ # The nearest single-precision value, as C's double-to-float conversion
99
+ # gives it. `pack("e")` alone turns everything above FLT_MAX into
100
+ # Infinity, even values that round down to FLT_MAX, and every NaN into
101
+ # the same one.
102
+ def float32(f)
103
+ a = f.abs
104
+ if a > FLOAT_MAX && a < FLOAT_ROUNDS_TO_MAX
105
+ (f > 0) ? FLOAT_MAX : -FLOAT_MAX
106
+ elsif f.nan?
107
+ Wire.float_nan(f).unpack1("e")
108
+ else
109
+ [f].pack("e").unpack1("e")
110
+ end
111
+ end
112
+
113
+ def string(info, value)
114
+ raise_type(info, value, "a String") unless value.is_a?(String)
115
+ enc = value.encoding
116
+ if enc == UTF_8
117
+ raise_type(info, value, "valid UTF-8") unless value.valid_encoding?
118
+ value.frozen? ? value : value.dup.freeze
119
+ elsif enc == BINARY || enc == Encoding::US_ASCII
120
+ s = value.dup.force_encoding(UTF_8)
121
+ raise_type(info, value, "valid UTF-8") unless s.valid_encoding?
122
+ s.freeze
123
+ else
124
+ begin
125
+ value.encode(UTF_8).freeze
126
+ rescue EncodingError
127
+ raise_type(info, value, "a String convertible to UTF-8")
128
+ end
129
+ end
130
+ end
131
+
132
+ def enum(info, value)
133
+ if value.is_a?(Symbol)
134
+ number = info.enum_numbers[value]
135
+ raise_type(info, value, "a value of #{info.enum_module.descriptor.full_name}") unless number
136
+ info.enum_names.fetch(number)
137
+ elsif value.is_a?(Integer)
138
+ name = info.enum_names[value]
139
+ return name if name
140
+ if info.closed_enum || !ENUM_RANGE.cover?(value)
141
+ raise_type(info, value, "a value of #{info.enum_module.descriptor.full_name}")
142
+ end
143
+ value
144
+ else
145
+ raise_type(info, value, "a Symbol or Integer")
146
+ end
147
+ end
148
+
149
+ # Whether a value found in a repeated field or map is one the encoder can
150
+ # write. Those containers are plain Arrays and Hashes that callers may
151
+ # have filled directly, past the writers' checks.
152
+ def storable?(info, value)
153
+ case (kind = info.kind)
154
+ when FieldInfo::DOUBLE, FieldInfo::FLOAT then value.is_a?(Float) || value.is_a?(Integer)
155
+ when FieldInfo::BOOL then value == true || value == false
156
+ when FieldInfo::STRING, FieldInfo::BYTES then value.is_a?(String)
157
+ when FieldInfo::ENUM
158
+ value.is_a?(Symbol) ? info.enum_numbers.key?(value) : (value.is_a?(Integer) && ENUM_RANGE.cover?(value))
159
+ when FieldInfo::MESSAGE, FieldInfo::GROUP then value.instance_of?(info.message_class)
160
+ else value.is_a?(Integer) && RANGES.fetch(kind).cover?(value)
161
+ end
162
+ end
163
+
164
+ def raise_type(info, value, expected)
165
+ shown = value.inspect
166
+ shown = "#{shown[0, 60]}..." if shown.length > 64
167
+ raise Protostar::TypeError, "#{info.descriptor.full_name} expects #{expected}, got #{shown}"
168
+ end
169
+ end
170
+ end
171
+ end