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,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Protostar
4
+ class Compiler
5
+ # protoc's StripSourceRetentionOptions (`compiler/retention.cc`): a copy
6
+ # of a file without the options declared `retention = RETENTION_SOURCE`,
7
+ # without options messages left empty by that, and without the source
8
+ # locations under anything removed. protoc does this when it writes a
9
+ # descriptor set or embeds descriptors in generated code; its pool keeps
10
+ # the options for validating later files.
11
+ #
12
+ # @api private
13
+ module Retention
14
+ # descriptor.proto field numbers of each descriptor's `options`
15
+ OPTIONS_FIELD = {
16
+ Schema::File => 8, Schema::Message => 7, Schema::Field => 8, Schema::Oneof => 2, Schema::Enum => 3,
17
+ Schema::EnumValue => 3, Schema::Service => 3, Schema::Method => 4, Schema::ExtensionRange => 3
18
+ }.freeze
19
+
20
+ # @param file [Schema::File]
21
+ # @param pool [Schema::Pool] holds the stripped copies of the file's imports
22
+ # @param options_type [#call] options message full name to its {Schema::Message}
23
+ # @return [Schema::File] the stripped copy, not added to the pool
24
+ def self.strip(file, pool, options_type)
25
+ stripped = []
26
+ kept = {}.compare_by_identity
27
+ source_paths(file).each do |descriptor, path|
28
+ options = descriptor.options
29
+ next unless options.declared?
30
+ path += [OPTIONS_FIELD.fetch(descriptor.class)]
31
+ value = strip_value(options, options_type.call(options.type_name), path, stripped)
32
+ if !options.empty? && value.empty?
33
+ stripped << path
34
+ next
35
+ end
36
+ kept[descriptor] = value
37
+ end
38
+ info = file.source_info
39
+ if info && !stripped.empty?
40
+ info = Schema::SourceInfo.new(info.locations.reject { |loc| stripped.any? { |s| loc.path[0, s.size] == s } })
41
+ end
42
+ Schema::Draft.copy(file, pool: pool, source_info: info) { |descriptor| kept[descriptor] }
43
+ end
44
+
45
+ def self.strip_value(value, type, path, stripped)
46
+ fields = {}
47
+ value.fields.each do |name, v|
48
+ field = type&.fields_by_name&.[](name.to_s)
49
+ result = field ? strip_field(field, v, path, stripped) : v
50
+ fields[name] = result unless result.equal?(DROP)
51
+ end
52
+ extensions = {}
53
+ value.extensions.each do |field, v|
54
+ result = strip_field(field, v, path, stripped)
55
+ extensions[field] = result unless result.equal?(DROP)
56
+ end
57
+ Schema::MessageValue.new(fields, extensions, value.unknown_fields)
58
+ end
59
+
60
+ DROP = Object.new.freeze
61
+
62
+ def self.strip_field(field, value, path, stripped)
63
+ here = path + [field.number]
64
+ if field.options.retention == :RETENTION_SOURCE
65
+ stripped << here
66
+ return DROP
67
+ end
68
+ return value unless field.type == :message || field.type == :group
69
+ if field.repeated?
70
+ value.each_with_index.map { |v, i| strip_value(v, field.message_type, here + [i], stripped) }
71
+ else
72
+ strip_value(value, field.message_type, here, stripped)
73
+ end
74
+ end
75
+
76
+ # Each descriptor's path in its FileDescriptorProto, as source locations spell it
77
+ def self.source_paths(file)
78
+ paths = {file => []}.compare_by_identity
79
+ visit = lambda do |m, path|
80
+ paths[m] = path
81
+ m.fields.each_with_index { |f, i| paths[f] = path + [2, i] }
82
+ m.nested_messages.each_with_index { |n, i| visit.call(n, path + [3, i]) }
83
+ m.nested_enums.each_with_index { |e, i| enum_paths(paths, e, path + [4, i]) }
84
+ m.extension_range_descriptors.each_with_index { |r, i| paths[r] = path + [5, i] }
85
+ m.extensions.each_with_index { |f, i| paths[f] = path + [6, i] }
86
+ m.oneofs.each_with_index { |o, i| paths[o] = path + [8, i] }
87
+ end
88
+ file.messages.each_with_index { |m, i| visit.call(m, [4, i]) }
89
+ file.enums.each_with_index { |e, i| enum_paths(paths, e, [5, i]) }
90
+ file.services.each_with_index do |s, i|
91
+ paths[s] = [6, i]
92
+ s.rpcs.each_with_index { |m, j| paths[m] = [6, i, 2, j] }
93
+ end
94
+ file.extensions.each_with_index { |f, i| paths[f] = [7, i] }
95
+ paths
96
+ end
97
+
98
+ def self.enum_paths(paths, enum, path)
99
+ paths[enum] = path
100
+ enum.values.each_with_index { |v, i| paths[v] = path + [2, i] }
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Protostar
4
+ class Compiler
5
+ # One {Compiler#compile} call: loads the named files and their imports
6
+ # depth first, as protoc's importer does (a file is parsed, then its
7
+ # imports are built, then the file itself), and collects diagnostics in
8
+ # protoc's order.
9
+ #
10
+ # @api private
11
+ class Run
12
+ # @return [Array<Diagnostic>]
13
+ attr_reader :diagnostics
14
+
15
+ # @param compiler [Compiler]
16
+ # @param names [Array<String>] the files asked for
17
+ # @param source_info [Boolean]
18
+ # @param sources [Hash{String => String}] file contents to use instead of reading include paths
19
+ # @param protoc_cli [Boolean] behave like protoc's command line (see {Linker#initialize});
20
+ # false behaves like a bare DescriptorPool, which loads no imports itself
21
+ # @param all_direct [Boolean] check every file loaded as if it had been named, imports
22
+ # included (unused imports, deprecated features), as `protostar lint` wants
23
+ def initialize(compiler, names, source_info, sources: {}, protoc_cli: true, all_direct: false)
24
+ @compiler = compiler
25
+ @pool = compiler.pool
26
+ @names = names
27
+ @source_info = source_info
28
+ @sources = sources
29
+ @protoc_cli = protoc_cli
30
+ @all_direct = all_direct
31
+ @diagnostics = []
32
+ @pending = []
33
+ @done = {}
34
+ @failed = false
35
+ @order = []
36
+ @listed = {}
37
+ @linkers = {}
38
+ end
39
+
40
+ def failed?
41
+ @failed || @diagnostics.any?(&:error?)
42
+ end
43
+
44
+ # @return [Linker, nil] the linker of a file this run built
45
+ def linker(name)
46
+ @linkers[name]
47
+ end
48
+
49
+ # @return [Array<Schema::File>]
50
+ def call
51
+ direct = []
52
+ @names.each do |name|
53
+ file = load(name, direct: true)
54
+ if file.nil?
55
+ @failed = true
56
+ break
57
+ end
58
+ direct << @linkers[name] if @linkers[name]
59
+ end
60
+ Validator.cli_checks(direct, self) if @protoc_cli && !failed?
61
+ @order
62
+ end
63
+
64
+ # Records a diagnostic with a 0-based position (nil for none)
65
+ # @param spans [Array<Diagnostics::Span>] where the renderer underlines, when known
66
+ def report(file, position, message, severity = :error, spans: [])
67
+ line, column = position
68
+ line = nil if line && line < 0
69
+ path = @compiler.disk_path(file) if file && !@sources.key?(file) && @protoc_cli
70
+ @diagnostics << Diagnostic.new(file, line && line + 1, line && column + 1, message, severity, path: path,
71
+ spans: line ? spans : [])
72
+ end
73
+
74
+ private
75
+
76
+ # The problem's own span, as long as the source location that starts
77
+ # where it is reported, then the places it relates to
78
+ def spans(ast, problem, position)
79
+ return [] unless position
80
+ out = [span(ast, position, nil, true)]
81
+ problem.related.each do |node, kind, label|
82
+ at = node.position(kind)
83
+ out << span(ast, at, label, false) if at
84
+ end
85
+ out
86
+ end
87
+
88
+ def span(ast, position, label, primary)
89
+ line, column = position
90
+ end_line, end_column = SourceInfo.extent(ast.locations, line, column)
91
+ Diagnostics::Span.new(line + 1, column + 1, end_line && end_line + 1, end_column && end_column + 1,
92
+ label: label, primary: primary)
93
+ end
94
+
95
+ # An extension number another file already uses is a warning the linker
96
+ # reports as a diagnostic, so the pool is asked not to print it too
97
+ def add_to_pool(name, file)
98
+ @pool.add(file, warn: false)
99
+ rescue Schema::Error => e
100
+ report(name, nil, e.message)
101
+ nil
102
+ end
103
+
104
+ def list(file)
105
+ return if @listed[file.name]
106
+ file.dependency_files.each { |dep| list(dep) }
107
+ @listed[file.name] = true
108
+ @order << file
109
+ end
110
+
111
+ def read(name)
112
+ return @sources[name] if @sources.key?(name)
113
+ return nil unless @protoc_cli
114
+ path = @compiler.resolve(name)
115
+ path && ::File.binread(path)
116
+ end
117
+
118
+ # @return [Schema::File, nil]
119
+ def load(name, direct: false)
120
+ return @done[name] if @done.key?(name)
121
+ if direct && !@protoc_cli && @pool.include?(name)
122
+ ast = Parser.new(read(name).to_s).parse(name)
123
+ report(name, ast.position(:other), "A file with this name is already in the pool.")
124
+ return @done[name] = nil
125
+ end
126
+ if (existing = @pool.file(name))
127
+ list(existing)
128
+ return @done[name] = existing
129
+ end
130
+
131
+ if (from = @pending.index { |n, _| n == name })
132
+ chain = @pending[from..].map(&:first) + [name]
133
+ ast = @pending[from][1]
134
+ import = (from < @pending.size - 1) ? @pending[from + 1][0] : name
135
+ report(name, ast.import_positions[import], "File recursively imports itself: #{chain.join(" -> ")}")
136
+ return nil
137
+ end
138
+
139
+ source = read(name)
140
+ unless source
141
+ report(name, nil, "File not found.") if @protoc_cli
142
+ return @done[name] = nil
143
+ end
144
+
145
+ parser = Parser.new(source)
146
+ ast = parser.parse(name)
147
+ parser.errors.each { |line, column, message| report(name, [line, column], message) }
148
+ parser.warnings.each { |line, column, message| report(name, [line, column], message, :warning) }
149
+ return @done[name] = nil unless parser.errors.empty?
150
+
151
+ @pending << [name, ast]
152
+ missing = []
153
+ (ast.dependency + ast.option_dependency).each do |dep|
154
+ missing << dep if load(dep).nil?
155
+ end
156
+ @pending.pop
157
+ # protoc's BuildFile gives up quietly on a file that imports itself,
158
+ # once the recursion has been reported
159
+ return @done[name] = nil if ast.dependency.include?(name)
160
+
161
+ linker = Linker.new(ast, pool: @pool, missing: missing, direct: (direct || @all_direct) && @protoc_cli,
162
+ options_fallback: ->(type) { @compiler.builtin_options_type(type) }, source_info: @source_info,
163
+ protoc_cli: @protoc_cli)
164
+ linker.link
165
+ Validator.new(linker).validate
166
+ linker.problems.each do |problem|
167
+ position = if problem.location == :import
168
+ ast.import_positions[problem.import]
169
+ else
170
+ problem.node&.position(problem.location)
171
+ end
172
+ report(name, position, problem.message, problem.severity, spans: spans(ast, problem, position))
173
+ end
174
+ file = linker.errors? ? nil : linker.file
175
+ file = add_to_pool(name, file) if file
176
+ if file
177
+ @linkers[name] = linker
178
+ list(file)
179
+ end
180
+ @done[name] = file
181
+ end
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Protostar
4
+ class Compiler
5
+ # Builds `SourceCodeInfo` the way protoc does: the parser opens a
6
+ # {Recorder} for every element as it starts reading it, so locations come
7
+ # out in pre-order with protoc's paths and spans, and comments are
8
+ # attached at the end of each declaration. After option interpretation
9
+ # {.rewrite_option_paths} moves option locations from
10
+ # `uninterpreted_option` paths to the paths of the options they set.
11
+ #
12
+ # @api private
13
+ module SourceInfo
14
+ # A `SourceCodeInfo.Location` under construction
15
+ class Location
16
+ attr_accessor :path, :span, :leading_comments, :trailing_comments, :leading_detached_comments
17
+
18
+ def initialize(path, span)
19
+ @path = path
20
+ @span = span
21
+ @leading_comments = nil
22
+ @trailing_comments = nil
23
+ @leading_detached_comments = []
24
+ end
25
+
26
+ def dup_with_path(path)
27
+ copy = Location.new(path, @span.dup)
28
+ copy.leading_comments = @leading_comments
29
+ copy.trailing_comments = @trailing_comments
30
+ copy.leading_detached_comments = @leading_detached_comments.dup
31
+ copy
32
+ end
33
+
34
+ # @return [Schema::SourceInfo::Location] the finished location, comments as UTF-8
35
+ def to_schema
36
+ Schema::SourceInfo::Location.new(path: @path, span: @span, leading_comments: utf8(@leading_comments),
37
+ trailing_comments: utf8(@trailing_comments),
38
+ leading_detached_comments: @leading_detached_comments.map { |c| utf8(c) })
39
+ end
40
+
41
+ private
42
+
43
+ def utf8(text)
44
+ text&.dup&.force_encoding(Encoding::UTF_8)
45
+ end
46
+ end
47
+
48
+ # protoc's Parser::LocationRecorder. It starts at the current token when
49
+ # created and, unless ended explicitly, ends at the previous token when
50
+ # {#finish}ed.
51
+ class Recorder
52
+ # @return [Location]
53
+ attr_reader :location
54
+ # @return [Array<Location>] where the location was added
55
+ attr_reader :target
56
+
57
+ # @param lexer [Lexer]
58
+ # @param path [Array<Integer>] the parent's path plus this element's components
59
+ # @param target [Array<Location>]
60
+ def initialize(lexer, path, target)
61
+ @lexer = lexer
62
+ @target = target
63
+ token = lexer.current
64
+ @location = Location.new(path, [token.line, token.column])
65
+ target << @location
66
+ end
67
+
68
+ # @return [Recorder] a child location, its path extended by `components`
69
+ def child(*components, target: @target)
70
+ Recorder.new(@lexer, @location.path + components, target)
71
+ end
72
+
73
+ def add_path(component)
74
+ @location.path << component
75
+ end
76
+
77
+ def path_size
78
+ @location.path.size
79
+ end
80
+
81
+ def start_at(token)
82
+ @location.span[0] = token.line
83
+ @location.span[1] = token.column
84
+ end
85
+
86
+ def start_at_recorder(other)
87
+ @location.span[0] = other.location.span[0]
88
+ @location.span[1] = other.location.span[1]
89
+ end
90
+
91
+ def end_at(token)
92
+ @location.span << token.line if token.line != @location.span[0]
93
+ @location.span << token.end_column
94
+ end
95
+
96
+ # The LocationRecorder destructor
97
+ def finish
98
+ end_at(@lexer.previous) if @location.span.size <= 2
99
+ end
100
+
101
+ # Records where this element starts, for diagnostics against `node`
102
+ def record_legacy(node, kind)
103
+ node.positions[kind] = [@location.span[0], @location.span[1]]
104
+ end
105
+
106
+ # @return [Array(Integer, Integer)]
107
+ def start
108
+ [@location.span[0], @location.span[1]]
109
+ end
110
+
111
+ def attach_comments(leading, trailing, detached)
112
+ @location.leading_comments = leading.dup unless leading.empty?
113
+ @location.trailing_comments = trailing.dup unless trailing.empty?
114
+ detached.each { |c| @location.leading_detached_comments << c }
115
+ detached.clear
116
+ end
117
+ end
118
+
119
+ # Where the shortest location starting at a position ends: the extent of
120
+ # the element whose name, number or type is reported there
121
+ # @param locations [Array<Location>]
122
+ # @return [Array(Integer, Integer), nil] 0-based line and column just past the end
123
+ def self.extent(locations, line, column)
124
+ best = nil
125
+ locations.each do |loc|
126
+ span = loc.span
127
+ next unless span[0] == line && span[1] == column && span.size >= 3
128
+ finish = (span.size == 4) ? [span[2], span[3]] : [span[0], span[2]]
129
+ best = finish if best.nil? || (finish <=> best) < 0
130
+ end
131
+ best
132
+ end
133
+
134
+ # OptionInterpreter::UpdateSourceCodeInfo: the location of each
135
+ # interpreted option takes the path of the option it set, and the
136
+ # locations nested under it (name parts, values) are dropped
137
+ # @param locations [Array<Location>]
138
+ # @param interpreted [Hash{Array<Integer> => Array<Integer>}] uninterpreted_option path to option path
139
+ # @return [Array<Location>]
140
+ def self.rewrite_option_paths(locations, interpreted)
141
+ return locations if interpreted.empty?
142
+ out = []
143
+ match_src = nil
144
+ locations.each do |loc|
145
+ if match_src
146
+ next if loc.path.size >= match_src.size && loc.path[0, match_src.size] == match_src
147
+ match_src = nil
148
+ end
149
+ dest = interpreted[loc.path]
150
+ if dest.nil?
151
+ out << loc
152
+ next
153
+ end
154
+ match_src = loc.path
155
+ out << loc.dup_with_path(dest.dup)
156
+ end
157
+ out
158
+ end
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,223 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Protostar
4
+ class Compiler
5
+ # The names visible while one file is linked: its own symbols, drafted
6
+ # as the linker meets them, and everything in the pool. Lookups follow
7
+ # protoc's DescriptorBuilder (FindSymbol and LookupSymbolNoPlaceholder):
8
+ # the same first-component scoping rule as {Schema::Resolver}, with a
9
+ # symbol in another file visible only through a direct import or a chain
10
+ # of `import public`, and with the failure explanations protoc gives.
11
+ #
12
+ # @api private
13
+ class SymbolTable
14
+ # A named thing
15
+ class Symbol
16
+ # @return [Symbol] :message, :enum, :enum_value, :field, :oneof, :service, :method or :package
17
+ attr_reader :kind
18
+ # @return [String]
19
+ attr_reader :full_name
20
+ # @return [String] the file defining it
21
+ attr_reader :file_name
22
+ # @return [Schema::Descriptor, nil] the descriptor (drafted, for the file being
23
+ # linked); nil for a package
24
+ attr_reader :object
25
+
26
+ def initialize(kind:, full_name:, file_name:, object:, local: false)
27
+ @kind = kind
28
+ @full_name = full_name
29
+ @file_name = file_name
30
+ @object = object
31
+ @local = local
32
+ freeze
33
+ end
34
+
35
+ def type?
36
+ @kind == :message || @kind == :enum
37
+ end
38
+
39
+ def aggregate?
40
+ @kind == :message || @kind == :package || @kind == :enum || @kind == :service
41
+ end
42
+
43
+ # @return [Boolean] defined by the file being linked
44
+ def local?
45
+ @local
46
+ end
47
+ end
48
+
49
+ SCHEMA_KINDS = {
50
+ Schema::Message => :message, Schema::Enum => :enum, Schema::EnumValue => :enum_value,
51
+ Schema::Field => :field, Schema::Oneof => :oneof, Schema::Service => :service, Schema::Method => :method
52
+ }.freeze
53
+
54
+ # @return [String, nil] set when the last failed lookup found the name in a file that is not imported
55
+ attr_reader :undeclared_file, :undeclared_name
56
+ # @return [String, nil] set when the last failed lookup resolved a compound name to a missing symbol
57
+ attr_reader :unresolved_name
58
+ # @return [Array<String>] direct imports no lookup has used yet
59
+ attr_reader :unused
60
+
61
+ # @param file_name [String] the file being linked
62
+ # @param package [String]
63
+ # @param pool [Schema::Pool]
64
+ # @param dependencies [Array<Schema::File, nil>] direct imports, nil where missing
65
+ # @param option_dependencies [Array<Schema::File, nil>] `import option` files
66
+ # @param unused [Array<Schema::File>] imports to report when never used
67
+ def initialize(file_name, package, pool, dependencies, option_dependencies, unused)
68
+ @file_name = file_name
69
+ @package = package
70
+ @pool = pool
71
+ @local = {}
72
+ @visible = {}
73
+ dependencies.each { |dep| record_public(dep, @visible) }
74
+ @option_visible = {}
75
+ option_dependencies.each { |dep| record_public(dep, @option_visible) }
76
+ @unused = unused.map(&:name)
77
+ @packages = {}
78
+ pool.files.each { |f| add_package_names(f.package, f.name) }
79
+ @local_packages = []
80
+ end
81
+
82
+ # @return [Hash{String => Symbol}] the file's own symbols
83
+ attr_reader :local
84
+
85
+ # Registers a symbol of the file being linked
86
+ # @param descriptor [Schema::Descriptor] as drafted
87
+ # @return [Symbol, nil] the existing symbol when the name is taken
88
+ def add(kind, full_name, descriptor)
89
+ existing = find_any(full_name)
90
+ return existing if existing
91
+ @local[full_name] = Symbol.new(kind: kind, full_name: full_name, file_name: @file_name, object: descriptor,
92
+ local: true)
93
+ nil
94
+ end
95
+
96
+ # Registers the file's package and its parents, as AddPackage does
97
+ # @return [Symbol, nil] a non-package symbol already using one of the names
98
+ def add_package(package)
99
+ existing = find_any(package)
100
+ if existing.nil?
101
+ @packages[package] ||= @file_name
102
+ @local_packages << package
103
+ cut = package.rindex(".")
104
+ return add_package(package[0, cut]) if cut
105
+ elsif existing.kind != :package
106
+ return existing
107
+ end
108
+ nil
109
+ end
110
+
111
+ # Any symbol by full name, ignoring visibility
112
+ # @return [Symbol, nil]
113
+ def find_any(full_name)
114
+ if (sym = @local[full_name])
115
+ return sym
116
+ end
117
+ if (descriptor = @pool.find_symbol(full_name))
118
+ return Symbol.new(kind: SCHEMA_KINDS.fetch(descriptor.class), full_name: full_name,
119
+ file_name: descriptor.file.name, object: descriptor)
120
+ end
121
+ if (owner = @packages[full_name])
122
+ return Symbol.new(kind: :package, full_name: full_name, file_name: owner, object: nil)
123
+ end
124
+ nil
125
+ end
126
+
127
+ # FindSymbolNotEnforcingDeps: any symbol, marking its file's import as used
128
+ def find_not_enforcing_deps(full_name)
129
+ sym = find_any(full_name)
130
+ if sym && sym.kind != :package && (@visible.key?(sym.file_name) || @option_visible.key?(sym.file_name))
131
+ @unused.delete(sym.file_name)
132
+ end
133
+ sym
134
+ end
135
+
136
+ # FindSymbol: a symbol visible from the file being linked, or nil (and
137
+ # {#undeclared_file} set) when it lives in a file that is not imported
138
+ def find(full_name)
139
+ sym = find_not_enforcing_deps(full_name)
140
+ return nil if sym.nil?
141
+ return sym if sym.file_name == @file_name || @visible.key?(sym.file_name)
142
+ return sym if @option_visible.key?(sym.file_name) && sym.kind == :field
143
+ if sym.kind == :package
144
+ return sym if in_package?(@package, full_name)
145
+ return sym if @visible.each_value.any? { |f| in_package?(f.package, full_name) }
146
+ return sym if @option_visible.each_value.any? { |f| in_package?(f.package, full_name) }
147
+ end
148
+ @undeclared_file = sym.file_name
149
+ @undeclared_name = full_name
150
+ nil
151
+ end
152
+
153
+ # LookupSymbolNoPlaceholder
154
+ # @param name [String] as written, maybe with a leading dot
155
+ # @param relative_to [String] full name of the element doing the lookup
156
+ # @param types_only [Boolean] skip simple names that are not types
157
+ # @return [Symbol, nil]
158
+ def lookup(name, relative_to, types_only: false)
159
+ @undeclared_file = nil
160
+ @undeclared_name = nil
161
+ @unresolved_name = nil
162
+ return find(name[1..]) if name.start_with?(".")
163
+
164
+ dot = name.index(".")
165
+ first = dot ? name[0, dot] : name
166
+ scope = relative_to.dup
167
+ loop do
168
+ cut = scope.rindex(".")
169
+ return find(name) if cut.nil?
170
+ scope = scope[0, cut]
171
+ candidate = "#{scope}.#{first}"
172
+ sym = find(candidate)
173
+ next if sym.nil?
174
+ if dot
175
+ next unless sym.aggregate?
176
+ full = "#{scope}.#{name}"
177
+ found = find(full)
178
+ @unresolved_name = full if found.nil?
179
+ return found
180
+ end
181
+ return sym unless types_only && !sym.type?
182
+ end
183
+ end
184
+
185
+ # AddNotDefinedError's messages for the last failed {#lookup}
186
+ # @return [Array<String>]
187
+ def not_defined_messages(name)
188
+ if @undeclared_file.nil? && @unresolved_name.nil?
189
+ return ["\"#{name}\" is not defined."]
190
+ end
191
+ messages = []
192
+ if @undeclared_file
193
+ messages << "\"#{@undeclared_name}\" seems to be defined in \"#{@undeclared_file}\", which is not " \
194
+ "imported by \"#{@file_name}\". To use it here, please add the necessary import."
195
+ end
196
+ if @unresolved_name
197
+ messages << "\"#{name}\" is resolved to \"#{@unresolved_name}\", which is not defined. The innermost " \
198
+ "scope is searched first in name resolution. Consider using a leading '.'(i.e., \".#{name}\") to start " \
199
+ "from the outermost scope."
200
+ end
201
+ messages
202
+ end
203
+
204
+ private
205
+
206
+ def record_public(file, into)
207
+ return if file.nil? || into.key?(file.name)
208
+ into[file.name] = file
209
+ file.public_dependencies.each { |i| record_public(file.dependency_files[i], into) }
210
+ end
211
+
212
+ def add_package_names(package, file_name)
213
+ return if package.empty?
214
+ parts = package.split(".")
215
+ parts.size.downto(1) { |n| @packages[parts.first(n).join(".")] ||= file_name }
216
+ end
217
+
218
+ def in_package?(package, name)
219
+ package == name || (package.start_with?(name) && package.getbyte(name.bytesize) == 46) # "."
220
+ end
221
+ end
222
+ end
223
+ end