protostar 0.1.0.beta1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +60 -0
- data/LICENSE.txt +21 -0
- data/README.md +647 -0
- data/THIRD_PARTY_NOTICES.md +61 -0
- data/exe/protoc-gen-protostar +7 -0
- data/exe/protostar +7 -0
- data/lib/protostar/builder/generator.rb +71 -0
- data/lib/protostar/builder/sink.rb +35 -0
- data/lib/protostar/builder.rb +965 -0
- data/lib/protostar/cli.rb +267 -0
- data/lib/protostar/codec/decode_generator.rb +199 -0
- data/lib/protostar/codec/decoder.rb +296 -0
- data/lib/protostar/codec/encode_generator.rb +224 -0
- data/lib/protostar/codec/encoder.rb +121 -0
- data/lib/protostar/codec/sizer.rb +91 -0
- data/lib/protostar/codec.rb +15 -0
- data/lib/protostar/compiler/ast.rb +193 -0
- data/lib/protostar/compiler/emitters/descriptor_set.rb +57 -0
- data/lib/protostar/compiler/emitters/google_ruby.rb +103 -0
- data/lib/protostar/compiler/emitters/grpc.rb +175 -0
- data/lib/protostar/compiler/emitters/protostar_rbs.rb +252 -0
- data/lib/protostar/compiler/emitters/rbs.rb +394 -0
- data/lib/protostar/compiler/emitters/ruby.rb +503 -0
- data/lib/protostar/compiler/emitters.rb +268 -0
- data/lib/protostar/compiler/include/LICENSE +32 -0
- data/lib/protostar/compiler/include/google/protobuf/any.proto +106 -0
- data/lib/protostar/compiler/include/google/protobuf/api.proto +229 -0
- data/lib/protostar/compiler/include/google/protobuf/compiler/plugin.proto +180 -0
- data/lib/protostar/compiler/include/google/protobuf/cpp_features.proto +87 -0
- data/lib/protostar/compiler/include/google/protobuf/descriptor.proto +1532 -0
- data/lib/protostar/compiler/include/google/protobuf/duration.proto +115 -0
- data/lib/protostar/compiler/include/google/protobuf/empty.proto +51 -0
- data/lib/protostar/compiler/include/google/protobuf/field_mask.proto +243 -0
- data/lib/protostar/compiler/include/google/protobuf/java_features.proto +132 -0
- data/lib/protostar/compiler/include/google/protobuf/source_context.proto +48 -0
- data/lib/protostar/compiler/include/google/protobuf/struct.proto +111 -0
- data/lib/protostar/compiler/include/google/protobuf/timestamp.proto +145 -0
- data/lib/protostar/compiler/include/google/protobuf/type.proto +217 -0
- data/lib/protostar/compiler/include/google/protobuf/wrappers.proto +157 -0
- data/lib/protostar/compiler/lexer.rb +659 -0
- data/lib/protostar/compiler/linker.rb +894 -0
- data/lib/protostar/compiler/option_interpreter.rb +363 -0
- data/lib/protostar/compiler/parser.rb +1696 -0
- data/lib/protostar/compiler/retention.rb +104 -0
- data/lib/protostar/compiler/run.rb +184 -0
- data/lib/protostar/compiler/source_info.rb +161 -0
- data/lib/protostar/compiler/symbol_table.rb +223 -0
- data/lib/protostar/compiler/text_format.rb +340 -0
- data/lib/protostar/compiler/validator.rb +944 -0
- data/lib/protostar/compiler.rb +304 -0
- data/lib/protostar/diagnostics/codes.rb +1307 -0
- data/lib/protostar/diagnostics/formats.rb +64 -0
- data/lib/protostar/diagnostics/renderer.rb +240 -0
- data/lib/protostar/diagnostics/source.rb +153 -0
- data/lib/protostar/diagnostics.rb +117 -0
- data/lib/protostar/dsl.rb +367 -0
- data/lib/protostar/enum.rb +70 -0
- data/lib/protostar/errors.rb +23 -0
- data/lib/protostar/google/protobuf/any_pb.rb +90 -0
- data/lib/protostar/google/protobuf/api_pb.rb +199 -0
- data/lib/protostar/google/protobuf/compiler/plugin_pb.rb +168 -0
- data/lib/protostar/google/protobuf/descriptor_pb.rb +1239 -0
- data/lib/protostar/google/protobuf/duration_pb.rb +99 -0
- data/lib/protostar/google/protobuf/empty_pb.rb +36 -0
- data/lib/protostar/google/protobuf/field_mask_pb.rb +228 -0
- data/lib/protostar/google/protobuf/source_context_pb.rb +33 -0
- data/lib/protostar/google/protobuf/struct_pb.rb +91 -0
- data/lib/protostar/google/protobuf/timestamp_pb.rb +129 -0
- data/lib/protostar/google/protobuf/type_pb.rb +202 -0
- data/lib/protostar/google/protobuf/wrappers_pb.rb +125 -0
- data/lib/protostar/google_protobuf_shim.rb +24 -0
- data/lib/protostar/json/encoder.rb +340 -0
- data/lib/protostar/json/errors.rb +34 -0
- data/lib/protostar/json/parser.rb +437 -0
- data/lib/protostar/json/path.rb +39 -0
- data/lib/protostar/json.rb +95 -0
- data/lib/protostar/lint/command.rb +197 -0
- data/lib/protostar/lint/config.rb +98 -0
- data/lib/protostar/lint/context.rb +126 -0
- data/lib/protostar/lint/directives.rb +56 -0
- data/lib/protostar/lint/fixer.rb +165 -0
- data/lib/protostar/lint/naming.rb +54 -0
- data/lib/protostar/lint/rule.rb +120 -0
- data/lib/protostar/lint/rules/comment_enum.rb +31 -0
- data/lib/protostar/lint/rules/comment_enum_value.rb +32 -0
- data/lib/protostar/lint/rules/comment_field.rb +33 -0
- data/lib/protostar/lint/rules/comment_message.rb +31 -0
- data/lib/protostar/lint/rules/comment_rpc.rb +31 -0
- data/lib/protostar/lint/rules/comment_service.rb +31 -0
- data/lib/protostar/lint/rules/edition_recommended.rb +31 -0
- data/lib/protostar/lint/rules/enum_first_value_zero.rb +34 -0
- data/lib/protostar/lint/rules/enum_pascal_case.rb +25 -0
- data/lib/protostar/lint/rules/enum_value_prefix.rb +38 -0
- data/lib/protostar/lint/rules/enum_value_upper_snake_case.rb +28 -0
- data/lib/protostar/lint/rules/enum_zero_value_suffix.rb +41 -0
- data/lib/protostar/lint/rules/field_lower_snake_case.rb +34 -0
- data/lib/protostar/lint/rules/field_not_required.rb +39 -0
- data/lib/protostar/lint/rules/field_number_reuse_risk.rb +45 -0
- data/lib/protostar/lint/rules/field_numbers_hot_range.rb +44 -0
- data/lib/protostar/lint/rules/file_lower_snake_case.rb +27 -0
- data/lib/protostar/lint/rules/import_no_public.rb +29 -0
- data/lib/protostar/lint/rules/import_no_weak.rb +28 -0
- data/lib/protostar/lint/rules/import_used.rb +33 -0
- data/lib/protostar/lint/rules/message_pascal_case.rb +27 -0
- data/lib/protostar/lint/rules/no_deprecated_features.rb +50 -0
- data/lib/protostar/lint/rules/no_groups.rb +34 -0
- data/lib/protostar/lint/rules/oneof_lower_snake_case.rb +27 -0
- data/lib/protostar/lint/rules/package_defined.rb +28 -0
- data/lib/protostar/lint/rules/package_directory_match.rb +33 -0
- data/lib/protostar/lint/rules/package_lower_snake_case.rb +28 -0
- data/lib/protostar/lint/rules/package_same_directory.rb +39 -0
- data/lib/protostar/lint/rules/package_version_suffix.rb +31 -0
- data/lib/protostar/lint/rules/reserved_names_with_numbers.rb +44 -0
- data/lib/protostar/lint/rules/rpc_pascal_case.rb +25 -0
- data/lib/protostar/lint/rules/rpc_request_response_unique.rb +40 -0
- data/lib/protostar/lint/rules/rpc_request_standard_name.rb +32 -0
- data/lib/protostar/lint/rules/rpc_response_standard_name.rb +31 -0
- data/lib/protostar/lint/rules/service_pascal_case.rb +25 -0
- data/lib/protostar/lint/rules/service_suffix.rb +28 -0
- data/lib/protostar/lint/rules/syntax_specified.rb +35 -0
- data/lib/protostar/lint/runner.rb +135 -0
- data/lib/protostar/lint.rb +122 -0
- data/lib/protostar/message/accessors.rb +86 -0
- data/lib/protostar/message/coercion.rb +171 -0
- data/lib/protostar/message/field_info.rb +145 -0
- data/lib/protostar/message/reflection.rb +149 -0
- data/lib/protostar/message.rb +572 -0
- data/lib/protostar/plugin.rb +94 -0
- data/lib/protostar/schema/default_value.rb +113 -0
- data/lib/protostar/schema/descriptor.rb +41 -0
- data/lib/protostar/schema/draft/copy.rb +88 -0
- data/lib/protostar/schema/draft/resolution.rb +177 -0
- data/lib/protostar/schema/draft.rb +779 -0
- data/lib/protostar/schema/enum.rb +106 -0
- data/lib/protostar/schema/extension_range.rb +37 -0
- data/lib/protostar/schema/features.rb +153 -0
- data/lib/protostar/schema/field.rb +201 -0
- data/lib/protostar/schema/file.rb +217 -0
- data/lib/protostar/schema/freezer.rb +61 -0
- data/lib/protostar/schema/message.rb +104 -0
- data/lib/protostar/schema/oneof.rb +38 -0
- data/lib/protostar/schema/options.rb +367 -0
- data/lib/protostar/schema/pool.rb +179 -0
- data/lib/protostar/schema/proto.rb +576 -0
- data/lib/protostar/schema/resolver.rb +118 -0
- data/lib/protostar/schema/service.rb +89 -0
- data/lib/protostar/schema/source_info.rb +53 -0
- data/lib/protostar/schema.rb +65 -0
- data/lib/protostar/short_names/any.rb +7 -0
- data/lib/protostar/short_names/duration.rb +7 -0
- data/lib/protostar/short_names/empty.rb +7 -0
- data/lib/protostar/short_names/field_mask.rb +7 -0
- data/lib/protostar/short_names/struct.rb +9 -0
- data/lib/protostar/short_names/timestamp.rb +7 -0
- data/lib/protostar/short_names/wrappers.rb +15 -0
- data/lib/protostar/text/extensions.rb +114 -0
- data/lib/protostar/text/parser.rb +439 -0
- data/lib/protostar/text/printer.rb +304 -0
- data/lib/protostar/text/tokenizer.rb +116 -0
- data/lib/protostar/text.rb +67 -0
- data/lib/protostar/types.rb +152 -0
- data/lib/protostar/unknown_field.rb +58 -0
- data/lib/protostar/version.rb +5 -0
- data/lib/protostar/view/generator.rb +298 -0
- data/lib/protostar/view/map.rb +194 -0
- data/lib/protostar/view/repeated.rb +143 -0
- data/lib/protostar/view/scan.rb +183 -0
- data/lib/protostar/view.rb +283 -0
- data/lib/protostar/well_known/any.rb +52 -0
- data/lib/protostar/well_known/duration.rb +53 -0
- data/lib/protostar/well_known/list_value.rb +63 -0
- data/lib/protostar/well_known/struct.rb +55 -0
- data/lib/protostar/well_known/timestamp.rb +64 -0
- data/lib/protostar/well_known/value.rb +60 -0
- data/lib/protostar/well_known.rb +38 -0
- data/lib/protostar/wire/io_source.rb +40 -0
- data/lib/protostar/wire/reader.rb +254 -0
- data/lib/protostar/wire/writer.rb +201 -0
- data/lib/protostar/wire.rb +96 -0
- data/lib/protostar.rb +95 -0
- data/shims/google-protobuf/lib/google/protobuf/any_pb.rb +17 -0
- data/shims/google-protobuf/lib/google/protobuf/api_pb.rb +22 -0
- data/shims/google-protobuf/lib/google/protobuf/compiler/plugin_pb.rb +25 -0
- data/shims/google-protobuf/lib/google/protobuf/descriptor_pb.rb +72 -0
- data/shims/google-protobuf/lib/google/protobuf/descriptor_pool.rb +164 -0
- data/shims/google-protobuf/lib/google/protobuf/descriptors.rb +553 -0
- data/shims/google-protobuf/lib/google/protobuf/duration_pb.rb +17 -0
- data/shims/google-protobuf/lib/google/protobuf/empty_pb.rb +17 -0
- data/shims/google-protobuf/lib/google/protobuf/field_mask_pb.rb +17 -0
- data/shims/google-protobuf/lib/google/protobuf/internal/convert.rb +180 -0
- data/shims/google-protobuf/lib/google/protobuf/internal/extensions.rb +154 -0
- data/shims/google-protobuf/lib/google/protobuf/internal/json.rb +104 -0
- data/shims/google-protobuf/lib/google/protobuf/internal/well_known.rb +140 -0
- data/shims/google-protobuf/lib/google/protobuf/map.rb +216 -0
- data/shims/google-protobuf/lib/google/protobuf/message.rb +544 -0
- data/shims/google-protobuf/lib/google/protobuf/message_exts.rb +41 -0
- data/shims/google-protobuf/lib/google/protobuf/plugin_pb.rb +4 -0
- data/shims/google-protobuf/lib/google/protobuf/repeated_field.rb +366 -0
- data/shims/google-protobuf/lib/google/protobuf/source_context_pb.rb +17 -0
- data/shims/google-protobuf/lib/google/protobuf/struct_pb.rb +20 -0
- data/shims/google-protobuf/lib/google/protobuf/timestamp_pb.rb +17 -0
- data/shims/google-protobuf/lib/google/protobuf/type_pb.rb +27 -0
- data/shims/google-protobuf/lib/google/protobuf/well_known_types.rb +37 -0
- data/shims/google-protobuf/lib/google/protobuf/wrappers_pb.rb +25 -0
- data/shims/google-protobuf/lib/google/protobuf.rb +107 -0
- data/shims/google-protobuf/lib/google/protobuf_ffi.rb +5 -0
- data/shims/google-protobuf/lib/google/protobuf_native.rb +5 -0
- data/sig/protostar/google/protobuf/any_pb.rbs +77 -0
- data/sig/protostar/google/protobuf/api_pb.rbs +242 -0
- data/sig/protostar/google/protobuf/compiler/plugin_pb.rbs +264 -0
- data/sig/protostar/google/protobuf/descriptor_pb.rbs +2220 -0
- data/sig/protostar/google/protobuf/duration_pb.rbs +82 -0
- data/sig/protostar/google/protobuf/empty_pb.rbs +18 -0
- data/sig/protostar/google/protobuf/field_mask_pb.rbs +170 -0
- data/sig/protostar/google/protobuf/source_context_pb.rbs +24 -0
- data/sig/protostar/google/protobuf/struct_pb.rbs +135 -0
- data/sig/protostar/google/protobuf/timestamp_pb.rbs +102 -0
- data/sig/protostar/google/protobuf/type_pb.rbs +304 -0
- data/sig/protostar/google/protobuf/wrappers_pb.rbs +169 -0
- data/sig/protostar/lint.rbs +216 -0
- data/sig/protostar.rbs +326 -0
- metadata +366 -0
|
@@ -0,0 +1,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
|