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,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Protostar
6
+ module Diagnostics
7
+ # Diagnostics for machines: one JSON object per diagnostic (JSON Lines),
8
+ # or GitHub Actions workflow commands, which annotate the lines of a pull
9
+ # request.
10
+ module Formats
11
+ # @param diagnostic [Compiler::Diagnostic]
12
+ # @param source [Source, nil] the file's text, to find where spans end
13
+ # @return [Hash] code, severity, message, file, path, line, column, spans,
14
+ # help, notes, fix and rule; the message is the short title, `protoc` the
15
+ # compiler's own wording when that differs
16
+ def self.record(diagnostic, source: nil)
17
+ d = diagnostic.annotated
18
+ spans = d.spans
19
+ spans = [Span.new(d.line, d.column)] if spans.empty? && d.line
20
+ spans = spans.map { |s| source.resolve(s) } if source
21
+ out = {code: d.code, severity: d.severity.to_s, message: d.title}
22
+ out[:protoc] = d.message if d.title != d.message
23
+ out.merge!(file: d.file, path: d.path, line: d.line, column: d.column, spans: spans.map(&:to_h),
24
+ help: d.help, notes: d.notes, fix: d.fix&.to_h, rule: d.rule)
25
+ out
26
+ end
27
+
28
+ # @return [String] one line of JSON
29
+ def self.json(diagnostic, source: nil)
30
+ ::JSON.generate(record(diagnostic, source: source))
31
+ end
32
+
33
+ # @return [String] an `::error` or `::warning` workflow command
34
+ def self.github(diagnostic, source: nil)
35
+ d = diagnostic.annotated
36
+ props = {"file" => d.path || d.file}
37
+ if d.line
38
+ span = d.spans.first || Span.new(d.line, d.column)
39
+ span = source.resolve(span) if source
40
+ props["line"] = span.line
41
+ props["col"] = span.column
42
+ if span.end_line
43
+ props["endLine"] = span.end_line
44
+ props["endColumn"] = span.end_column
45
+ end
46
+ end
47
+ props["title"] = d.code if d.code
48
+ text = [d.title, *d.help.map { |h| "help: #{h}" }, *d.notes.map { |n| "note: #{n}" }]
49
+ text << "fix: #{d.fix.description}" if d.fix
50
+ kind = d.error? ? "error" : "warning"
51
+ "::#{kind} #{props.map { |k, v| "#{k}=#{escape_property(v.to_s)}" }.join(",")}::#{escape_data(text.join("\n"))}"
52
+ end
53
+
54
+ def self.escape_data(text)
55
+ text.gsub("%", "%25").gsub("\r", "%0D").gsub("\n", "%0A")
56
+ end
57
+
58
+ def self.escape_property(text)
59
+ escape_data(text).gsub(":", "%3A").gsub(",", "%2C")
60
+ end
61
+ private_class_method :escape_data, :escape_property
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,240 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Protostar
4
+ module Diagnostics
5
+ # Prints a {Compiler::Diagnostic} the way rustc prints its own:
6
+ #
7
+ # error[L0301]: field number 1 is already used in `p.A`
8
+ # --> link.proto:5:15
9
+ # |
10
+ # 4 | Missing m = 1;
11
+ # | - first used by `m`
12
+ # 5 | int32 dup = 1;
13
+ # | ^ reused here
14
+ # |
15
+ # = help: the next available field number is 2
16
+ #
17
+ # The primary span gets carets, related spans dashes, each with its label;
18
+ # a span over several lines is drawn down the left margin. Without the
19
+ # source, only the header, location and the `help:` and `note:` lines print.
20
+ class Renderer
21
+ COLORS = {
22
+ error: "\e[1;31m", warning: "\e[1;33m", secondary: "\e[1;34m", gutter: "\e[1;34m", bold: "\e[1m",
23
+ help: "\e[1;36m", reset: "\e[0m"
24
+ }.freeze
25
+ # Lines of a multi-line span shown between its first and last before `...`
26
+ INNER_LINES = 3
27
+
28
+ # @param color [Boolean] ANSI colours, for a terminal
29
+ # @param source [#call, nil] given the diagnostic, returns its file's text (or nil)
30
+ def initialize(color: false, source: nil)
31
+ @color = color
32
+ @source = source
33
+ @texts = {}
34
+ end
35
+
36
+ # @param diagnostic [Compiler::Diagnostic]
37
+ # @return [String] the rendered diagnostic, ending in a newline
38
+ def render(diagnostic)
39
+ d = diagnostic.annotated
40
+ out = +""
41
+ out << header(d) << "\n"
42
+ source = source_for(d)
43
+ spans = d.line ? spans_of(d, source) : []
44
+ width = [*spans.flat_map { |s| [s.line, s.end_line] }, d.line || 0].max.to_s.size
45
+ pad = " " * width
46
+ if d.line
47
+ out << "#{pad}#{paint("-->", :gutter)} #{d.path || d.file}:#{d.line}:#{d.column}\n"
48
+ elsif d.file
49
+ out << "#{pad}#{paint("-->", :gutter)} #{d.path || d.file}\n"
50
+ end
51
+ if source && !spans.empty?
52
+ out << "#{pad} #{paint("|", :gutter)}\n"
53
+ snippet(out, source, spans, d.severity, width)
54
+ end
55
+ footer = d.help.map { |h| ["help", h] } + d.notes.map { |n| ["note", n] }
56
+ footer << ["fix", d.fix.description] if d.fix
57
+ unless footer.empty?
58
+ out << "#{pad} #{paint("|", :gutter)}\n" if source && !spans.empty?
59
+ footer.each do |kind, text|
60
+ first, *rest = text.split("\n")
61
+ out << "#{pad} #{paint("=", :gutter)} #{paint("#{kind}:", (kind == "note") ? :bold : :help)} #{first}\n"
62
+ rest.each { |line| out << "#{pad} #{" " * (kind.size + 2)}#{line}\n" }
63
+ end
64
+ end
65
+ out
66
+ end
67
+
68
+ # @param diagnostic [Compiler::Diagnostic]
69
+ # @return [Source, nil]
70
+ def source_for(diagnostic)
71
+ return nil unless @source && diagnostic.file
72
+ key = diagnostic.path || diagnostic.file
73
+ return @texts[key] if @texts.key?(key)
74
+ text = @source.call(diagnostic)
75
+ @texts[key] = text && Source.new(text)
76
+ end
77
+
78
+ private
79
+
80
+ def header(d)
81
+ label = d.error? ? "error" : "warning"
82
+ label += "[#{d.code}]" if d.code
83
+ "#{paint(label, d.severity)}#{paint(": #{d.title}", :bold)}"
84
+ end
85
+
86
+ def paint(text, kind)
87
+ return text unless @color
88
+ "#{COLORS.fetch(kind)}#{text}#{COLORS[:reset]}"
89
+ end
90
+
91
+ def spans_of(d, source)
92
+ spans = d.spans.empty? ? [Span.new(d.line, d.column)] : d.spans
93
+ return spans unless source
94
+ spans.map do |span|
95
+ span = source.resolve(span)
96
+ next span if span.end_line > span.line || span.end_column > span.column
97
+ Span.new(span.line, span.column, span.line, span.column + 1, label: span.label, primary: span.primary?)
98
+ end
99
+ end
100
+
101
+ # One underline on one line, in display columns
102
+ class Mark
103
+ attr_reader :start, :finish, :primary, :label
104
+
105
+ def initialize(start, finish, primary, label)
106
+ @start = start
107
+ @finish = finish
108
+ @primary = primary
109
+ @label = label
110
+ end
111
+ end
112
+
113
+ def snippet(out, source, spans, severity, width)
114
+ multi = spans.find { |s| s.end_line > s.line }
115
+ multi = nil if multi && multi.end_line > source.size + 1
116
+ marks = Hash.new { |h, k| h[k] = [] }
117
+ spans.each do |span|
118
+ next if span.equal?(multi)
119
+ finish = (span.end_line == span.line) ? span.end_column : source.end_column(span.line)
120
+ start = source.display_column(span.line, span.column)
121
+ stop = [source.display_column(span.line, finish), start + 1].max
122
+ marks[span.line] << Mark.new(start, stop, span.primary?, span.label)
123
+ end
124
+ shown = marks.keys
125
+ if multi
126
+ inner = ((multi.line + 1)...multi.end_line).to_a
127
+ inner = inner.first(INNER_LINES) + [nil] if inner.size > INNER_LINES + 1
128
+ shown += [multi.line, multi.end_line] + inner.compact
129
+ end
130
+ shown = shown.uniq.sort
131
+ margin = multi ? 2 : 0
132
+ prev = nil
133
+ shown.each do |line|
134
+ if prev && line > prev + 1
135
+ if line == prev + 2
136
+ source_line(out, source, prev + 1, width, gutter_margin(multi, prev + 1, source, severity), margin)
137
+ else
138
+ out << "#{paint("...", :gutter)}\n"
139
+ end
140
+ end
141
+ source_line(out, source, line, width, gutter_margin(multi, line, source, severity), margin)
142
+ if multi && line == multi.line && !slash_start?(multi, source)
143
+ col = source.display_column(line, multi.column)
144
+ row = " #{paint("_" * col + marker(multi), multi.primary? ? severity : :secondary)}"
145
+ out << "#{" " * width} #{paint("|", :gutter)} #{row}\n"
146
+ end
147
+ marks[line].empty? || annotate(out, marks[line], width, inside?(multi, line) ? :bar : :blank, severity, margin)
148
+ if multi && line == multi.end_line
149
+ last = [source.display_column(line, multi.end_column) - 1, 1].max
150
+ kind = multi.primary? ? severity : :secondary
151
+ row = paint("|#{"_" * last}#{marker(multi)}", kind)
152
+ row += " #{paint(multi.label, kind)}" if multi.label
153
+ out << "#{" " * width} #{paint("|", :gutter)} #{row}\n"
154
+ end
155
+ prev = line
156
+ end
157
+ end
158
+
159
+ def slash_start?(multi, source)
160
+ source.first_column(multi.line) == multi.column
161
+ end
162
+
163
+ def inside?(multi, line)
164
+ multi && line > multi.line && line <= multi.end_line
165
+ end
166
+
167
+ def gutter_margin(multi, line, source, severity)
168
+ return "" unless multi
169
+ kind = multi.primary? ? severity : :secondary
170
+ if line == multi.line
171
+ slash_start?(multi, source) ? paint("/", kind) + " " : " "
172
+ elsif line > multi.line && line <= multi.end_line
173
+ paint("|", kind) + " "
174
+ else
175
+ " "
176
+ end
177
+ end
178
+
179
+ def marker(span)
180
+ span.primary? ? "^" : "-"
181
+ end
182
+
183
+ def source_line(out, source, line, width, margin_text, margin)
184
+ text = source.display(line)
185
+ number = paint(line.to_s.rjust(width), :gutter)
186
+ body = margin.zero? ? text : margin_text + text
187
+ out << "#{number} #{paint("|", :gutter)}#{" #{body}" unless body.empty?}\n"
188
+ end
189
+
190
+ # The underline row, and below it the labels that do not fit at its end,
191
+ # each hanging from its span by a `|`
192
+ def annotate(out, marks, width, margin_kind, severity, margin)
193
+ marks = marks.sort_by { |m| [m.start, m.primary ? 1 : 0] }
194
+ prefix = "#{" " * width} #{paint("|", :gutter)} "
195
+ lead = if margin.zero?
196
+ ""
197
+ else
198
+ (margin_kind == :bar) ? paint("|", severity) + " " : " "
199
+ end
200
+ cells = Array.new(marks.map(&:finish).max - 1, " ")
201
+ kinds = Array.new(cells.size)
202
+ marks.each do |m|
203
+ (m.start...m.finish).each do |c|
204
+ next if kinds[c - 1] && !m.primary
205
+ cells[c - 1] = m.primary ? "^" : "-"
206
+ kinds[c - 1] = m.primary ? severity : :secondary
207
+ end
208
+ end
209
+ labelled = marks.select(&:label)
210
+ last = labelled.max_by(&:start)
211
+ row = paint_cells(cells, kinds)
212
+ row += " #{paint(last.label, last.primary ? severity : :secondary)}" if last && last.start == marks.map(&:start).max
213
+ out << "#{prefix}#{lead}#{row.rstrip}\n"
214
+ hanging = labelled.reject { |m| m.equal?(last) && last.start == marks.map(&:start).max }.sort_by(&:start)
215
+ until hanging.empty?
216
+ connectors = Array.new(hanging.last.start, " ")
217
+ ckinds = Array.new(connectors.size)
218
+ hanging.each do |m|
219
+ connectors[m.start - 1] = "|"
220
+ ckinds[m.start - 1] = m.primary ? severity : :secondary
221
+ end
222
+ out << "#{prefix}#{lead}#{paint_cells(connectors, ckinds).rstrip}\n"
223
+ m = hanging.pop
224
+ labels = Array.new(m.start - 1, " ")
225
+ lkinds = Array.new(labels.size)
226
+ hanging.each do |h|
227
+ labels[h.start - 1] = "|"
228
+ lkinds[h.start - 1] = h.primary ? severity : :secondary
229
+ end
230
+ out << "#{prefix}#{lead}#{paint_cells(labels, lkinds)}#{paint(m.label, m.primary ? severity : :secondary)}\n"
231
+ end
232
+ end
233
+
234
+ def paint_cells(cells, kinds)
235
+ return cells.join unless @color
236
+ cells.each_with_index.map { |c, i| (kinds[i] && c != " ") ? paint(c, kinds[i]) : c }.join
237
+ end
238
+ end
239
+ end
240
+ end
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Protostar
4
+ module Diagnostics
5
+ # The text of one `.proto` file, read the way protoc counts positions:
6
+ # 1-based lines, columns in bytes with a tab advancing to the next
7
+ # multiple of 8. Maps those columns to what a terminal shows, finds the
8
+ # token at a position, and applies {Edit}s.
9
+ class Source
10
+ TAB_WIDTH = 8
11
+ TOKEN = /\A(?:[A-Za-z_][A-Za-z0-9_]*|0[xX][0-9A-Fa-f]+|[0-9]+(?:\.[0-9]*)?(?:[eE][+-]?[0-9]+)?[A-Za-z]?|
12
+ \.[0-9]+(?:[eE][+-]?[0-9]+)?|"(?:[^"\\]|\\.)*"?|'(?:[^'\\]|\\.)*'?)/x
13
+
14
+ # @return [Array<String>] the lines, without their line ends, as UTF-8
15
+ attr_reader :lines
16
+
17
+ # @param text [String] the file's bytes
18
+ def initialize(text)
19
+ utf8 = text.dup.force_encoding(Encoding::UTF_8)
20
+ utf8 = utf8.scrub("?") unless utf8.valid_encoding?
21
+ @lines = utf8.split("\n", -1).map { |l| l.delete_suffix("\r") }
22
+ @lines.pop if @lines.size > 1 && @lines.last.empty?
23
+ end
24
+
25
+ # @param number [Integer] 1-based; past the end is an empty line, where
26
+ # protoc reports errors at the end of input
27
+ # @return [String]
28
+ def line(number)
29
+ @lines.fetch(number - 1, "")
30
+ end
31
+
32
+ # @return [Integer] the number of lines
33
+ def size
34
+ @lines.size
35
+ end
36
+
37
+ # @return [String] the line as shown: tabs expanded to the columns protoc counts
38
+ def display(number)
39
+ out = +""
40
+ each_char(number) { |char, _, width, _| out << ((char == "\t") ? " " * width : char) }
41
+ out
42
+ end
43
+
44
+ # @param number [Integer] 1-based line
45
+ # @param column [Integer] 1-based protoc column
46
+ # @return [Integer] the 1-based column a terminal shows it at
47
+ def display_column(number, column)
48
+ target = column - 1
49
+ last_protoc = 0
50
+ last_display = 0
51
+ each_char(number) do |char, protoc, width, display|
52
+ if target < protoc + ((char == "\t") ? width : char.bytesize)
53
+ return display + 1 + ((char == "\t") ? target - protoc : 0)
54
+ end
55
+ last_protoc = protoc + ((char == "\t") ? width : char.bytesize)
56
+ last_display = display + ((char == "\t") ? width : 1)
57
+ end
58
+ last_display + (target - last_protoc) + 1
59
+ end
60
+
61
+ # @return [Integer] the 1-based protoc column just past the token starting at
62
+ # the position (one character when no token starts there)
63
+ def token_end(number, column)
64
+ text = line(number)
65
+ index = char_index(number, column)
66
+ return column + 1 if index.nil? || index >= text.size
67
+ rest = text[index..]
68
+ token = rest[TOKEN] || rest[0]
69
+ column + token.bytesize
70
+ end
71
+
72
+ # @return [Integer, nil] the 1-based column of the first character that is not
73
+ # whitespace, nil on a blank line
74
+ def first_column(number)
75
+ each_char(number) { |char, protoc, _, _| return protoc + 1 unless char.match?(/\s/) }
76
+ nil
77
+ end
78
+
79
+ # @return [Integer] the 1-based column just past the last character
80
+ def end_column(number)
81
+ protoc = 0
82
+ each_char(number) { |char, at, width, _| protoc = at + ((char == "\t") ? width : char.bytesize) }
83
+ protoc + 1
84
+ end
85
+
86
+ # @return [Span] with its end filled in: the token at its start when unknown
87
+ def resolve(span)
88
+ return span if span.end_line
89
+ Span.new(span.line, span.column, span.line, token_end(span.line, span.column), label: span.label,
90
+ primary: span.primary?)
91
+ end
92
+
93
+ # @param edits [Array<Edit>] on this text, not overlapping
94
+ # @return [String] the text with the edits made, line ends as they were
95
+ def apply(text, edits)
96
+ out = text.dup.force_encoding(Encoding::UTF_8)
97
+ starts = line_starts(out)
98
+ edits.sort_by { |e| [e.line, e.column] }.reverse_each do |edit|
99
+ from = offset(out, starts, edit.line, edit.column)
100
+ to = offset(out, starts, edit.end_line, edit.end_column)
101
+ out = out.byteslice(0, from) + edit.text + out.byteslice(to..)
102
+ end
103
+ out
104
+ end
105
+
106
+ private
107
+
108
+ def each_char(number)
109
+ protoc = 0
110
+ display = 0
111
+ line(number).each_char do |char|
112
+ width = (char == "\t") ? TAB_WIDTH - protoc % TAB_WIDTH : 1
113
+ yield char, protoc, width, display
114
+ protoc += (char == "\t") ? width : char.bytesize
115
+ display += width
116
+ end
117
+ end
118
+
119
+ def char_index(number, column)
120
+ target = column - 1
121
+ index = 0
122
+ each_char(number) do |char, protoc, width, _|
123
+ return index if target < protoc + ((char == "\t") ? width : char.bytesize)
124
+ index += 1
125
+ end
126
+ index
127
+ end
128
+
129
+ def line_starts(text)
130
+ bytes = text.b
131
+ starts = [0]
132
+ while (nl = bytes.index("\n", starts.last))
133
+ starts << nl + 1
134
+ end
135
+ starts
136
+ end
137
+
138
+ # The byte offset of a protoc position in the text
139
+ def offset(text, starts, number, column)
140
+ start = starts.fetch(number - 1) { return text.bytesize }
141
+ target = column - 1
142
+ protoc = 0
143
+ bytes = 0
144
+ line(number).each_char do |char|
145
+ return start + bytes if protoc >= target
146
+ protoc += (char == "\t") ? TAB_WIDTH - protoc % TAB_WIDTH : char.bytesize
147
+ bytes += char.bytesize
148
+ end
149
+ start + bytes
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Protostar
4
+ # Diagnostics the way `rustc` prints them: the source line with the span
5
+ # underlined, related spans with their own labels, `help:` and `note:`
6
+ # lines, a code that `protostar lint --explain` looks up, and fixes. Both
7
+ # the compiler's errors ({Compiler::Diagnostic}) and lint findings render
8
+ # through here.
9
+ #
10
+ # renderer = Protostar::Diagnostics::Renderer.new(source: ->(name) { File.read(name) })
11
+ # puts renderer.render(diagnostic)
12
+ #
13
+ # Lines and columns are 1-based and count as protoc counts them: bytes, a
14
+ # tab advancing to the next multiple of 8.
15
+ module Diagnostics
16
+ autoload :Codes, ::File.expand_path("diagnostics/codes", __dir__)
17
+ autoload :Source, ::File.expand_path("diagnostics/source", __dir__)
18
+ autoload :Renderer, ::File.expand_path("diagnostics/renderer", __dir__)
19
+ autoload :Formats, ::File.expand_path("diagnostics/formats", __dir__)
20
+
21
+ # A stretch of source a diagnostic points at
22
+ class Span
23
+ # @return [Integer] 1-based
24
+ attr_reader :line
25
+ # @return [Integer] 1-based
26
+ attr_reader :column
27
+ # @return [Integer, nil] 1-based; nil when only the start is known, which
28
+ # renders as the token at the start
29
+ attr_reader :end_line
30
+ # @return [Integer, nil] 1-based, just past the last character
31
+ attr_reader :end_column
32
+ # @return [String, nil] printed next to the underline
33
+ attr_reader :label
34
+
35
+ def initialize(line, column, end_line = nil, end_column = nil, label: nil, primary: true)
36
+ @line = line
37
+ @column = column
38
+ @end_line = end_line
39
+ @end_column = end_column
40
+ @label = label
41
+ @primary = primary
42
+ freeze
43
+ end
44
+
45
+ # The primary span gets the carets, the others dashes
46
+ def primary?
47
+ @primary
48
+ end
49
+
50
+ # @return [Span] the same span with another label
51
+ def with_label(label)
52
+ Span.new(@line, @column, @end_line, @end_column, label: label, primary: @primary)
53
+ end
54
+
55
+ # @return [Hash]
56
+ def to_h
57
+ {line: @line, column: @column, end_line: @end_line, end_column: @end_column, label: @label, primary: @primary}
58
+ end
59
+
60
+ def ==(other)
61
+ other.is_a?(Span) && other.to_h == to_h
62
+ end
63
+ alias_method :eql?, :==
64
+
65
+ def hash
66
+ to_h.hash
67
+ end
68
+ end
69
+
70
+ # One replacement in a file
71
+ class Edit
72
+ # @return [Integer] 1-based
73
+ attr_reader :line
74
+ # @return [Integer] 1-based
75
+ attr_reader :column
76
+ # @return [Integer]
77
+ attr_reader :end_line
78
+ # @return [Integer] 1-based, just past the replaced text; equal to {#column} to insert
79
+ attr_reader :end_column
80
+ # @return [String]
81
+ attr_reader :text
82
+
83
+ def initialize(line, column, end_line, end_column, text)
84
+ @line = line
85
+ @column = column
86
+ @end_line = end_line
87
+ @end_column = end_column
88
+ @text = text.frozen? ? text : text.dup.freeze
89
+ freeze
90
+ end
91
+
92
+ # @return [Hash]
93
+ def to_h
94
+ {line: @line, column: @column, end_line: @end_line, end_column: @end_column, text: @text}
95
+ end
96
+ end
97
+
98
+ # A mechanical fix: what it does, in words, and the edits that do it
99
+ class Fix
100
+ # @return [String] e.g. "rename `colorRed` to `color_red`"
101
+ attr_reader :description
102
+ # @return [Array<Edit>]
103
+ attr_reader :edits
104
+
105
+ def initialize(description, edits)
106
+ @description = description
107
+ @edits = edits.freeze
108
+ freeze
109
+ end
110
+
111
+ # @return [Hash]
112
+ def to_h
113
+ {description: @description, edits: @edits.map(&:to_h)}
114
+ end
115
+ end
116
+ end
117
+ end