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,197 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
|
|
5
|
+
module Protostar
|
|
6
|
+
module Lint
|
|
7
|
+
# `protostar lint`, the command line
|
|
8
|
+
#
|
|
9
|
+
# protostar lint proto/ # every *.proto under proto/, recursively
|
|
10
|
+
# protostar lint acme/person.proto --format json
|
|
11
|
+
# protostar lint proto/ --rules default,documentation --except FIELD_NOT_REQUIRED
|
|
12
|
+
# protostar lint --explain FIELD_NOT_REQUIRED
|
|
13
|
+
#
|
|
14
|
+
# Exits 0 when clean, 1 with warnings (0 with --warnings-ok), 2 with
|
|
15
|
+
# errors, including mistakes on the command line.
|
|
16
|
+
class Command
|
|
17
|
+
USAGE = "Usage: protostar lint [OPTION] PROTO_FILES_OR_DIRS\n protostar lint --explain CODE_OR_RULE"
|
|
18
|
+
FORMATS = %w[text json github].freeze
|
|
19
|
+
EXPLAIN_HINT = "For more about a code or rule, try `protostar lint --explain CODE`."
|
|
20
|
+
|
|
21
|
+
def initialize(out, err)
|
|
22
|
+
@out = out
|
|
23
|
+
@err = err
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @param argv [Array<String>]
|
|
27
|
+
# @return [Integer] the exit status
|
|
28
|
+
def run(argv)
|
|
29
|
+
opts = {}
|
|
30
|
+
inputs = parser(opts).parse(argv)
|
|
31
|
+
return explain(opts[:explain]) if opts[:explain]
|
|
32
|
+
return list_rules if opts[:list]
|
|
33
|
+
raise Error, "Missing input file or directory.\n#{USAGE}" if inputs.empty?
|
|
34
|
+
config = opts[:config] ? Config.load(opts[:config]) : Config.find(inputs)
|
|
35
|
+
rules = Lint.select(opts[:rules] || config.rules, except: (opts[:except] || []) + config.except)
|
|
36
|
+
except = ((opts[:except] || []) + config.except).map { |e| Lint.rule_id(e) }
|
|
37
|
+
format = opts.fetch(:format, "text")
|
|
38
|
+
raise Error, "Unknown format: #{format} (#{FORMATS.join(", ")})" unless FORMATS.include?(format)
|
|
39
|
+
include, names = targets(inputs, opts[:include], config)
|
|
40
|
+
raise Error, "No .proto files in #{inputs.join(", ")}." if names.empty?
|
|
41
|
+
lint = -> { Runner.new(include_paths: include, rules: rules, except: except, directives: config.allow_comment_directives?) }
|
|
42
|
+
runner = lint.call
|
|
43
|
+
result = runner.run(names)
|
|
44
|
+
changed = fix(result, runner, write: opts[:fix], show: opts[:fix] || opts[:diff],
|
|
45
|
+
diff_io: (format == "text") ? nil : @err)
|
|
46
|
+
# what is left is what the fixed files say now
|
|
47
|
+
if changed
|
|
48
|
+
runner = lint.call
|
|
49
|
+
result = runner.run(names)
|
|
50
|
+
end
|
|
51
|
+
remaining = result.diagnostics
|
|
52
|
+
show(remaining, runner, format, opts, result)
|
|
53
|
+
return 2 if remaining.any?(&:error?)
|
|
54
|
+
return 1 if remaining.any?(&:warning?) && !opts[:warnings_ok]
|
|
55
|
+
0
|
|
56
|
+
rescue Error, OptionParser::ParseError, Compiler::Error => e
|
|
57
|
+
@err.puts e.message
|
|
58
|
+
2
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def parser(opts)
|
|
64
|
+
OptionParser.new do |o|
|
|
65
|
+
o.banner = USAGE
|
|
66
|
+
o.on("-I", "--proto_path=PATH", "Directory to resolve names and imports against; repeatable, or colon-separated") do |v|
|
|
67
|
+
(opts[:include] ||= []).concat(v.split(":"))
|
|
68
|
+
end
|
|
69
|
+
o.on("--rules=LIST", "Sets (minimal, default, strict), categories or rule ids; default: default") do |v|
|
|
70
|
+
(opts[:rules] ||= []).concat(v.split(","))
|
|
71
|
+
end
|
|
72
|
+
o.on("--except=LIST", "Rule ids or codes to leave out") { |v| (opts[:except] ||= []).concat(v.split(",")) }
|
|
73
|
+
o.on("--format=FORMAT", "text (default), json (one object per line) or github (workflow annotations)") do |v|
|
|
74
|
+
opts[:format] = v
|
|
75
|
+
end
|
|
76
|
+
o.on("--color=WHEN", %w[auto always never], "auto (default: when stdout is a terminal and NO_COLOR is unset), always, never") do |v|
|
|
77
|
+
opts[:color] = v
|
|
78
|
+
end
|
|
79
|
+
o.on("--warnings-ok", "Exit 0 when there are warnings but no errors") { opts[:warnings_ok] = true }
|
|
80
|
+
o.on("--fix", "Apply the safe fixes (renames, a missing syntax line) and print the diff") { opts[:fix] = true }
|
|
81
|
+
o.on("--diff", "Print the diff --fix would apply, change nothing") { opts[:diff] = true }
|
|
82
|
+
o.on("--config=FILE", "Read FILE instead of looking for .protostar.yml") { |v| opts[:config] = v }
|
|
83
|
+
o.on("--explain=CODE", "Explain a code (L0301) or rule (FIELD_NOT_REQUIRED, W0101)") { |v| opts[:explain] = v }
|
|
84
|
+
o.on("--list-rules", "List the rules, their codes and sets") { opts[:list] = true }
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def explain(key)
|
|
89
|
+
if (entry = Diagnostics::Codes[key])
|
|
90
|
+
@out.puts "#{entry.code}: #{entry.summary}", "", entry.explanation
|
|
91
|
+
return 0
|
|
92
|
+
end
|
|
93
|
+
id = Lint.rule_id(key)
|
|
94
|
+
raise Error, "No code or rule #{key}." unless id
|
|
95
|
+
rule = Lint.rule_class(id)
|
|
96
|
+
@out.puts "#{rule.code}: #{id} (#{rule.tier} set, #{rule.category})", "", rule.reason, "", rule.explanation
|
|
97
|
+
0
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def list_rules
|
|
101
|
+
RULES.each do |id, (code, tier, category)|
|
|
102
|
+
@out.puts format("%-6s %-30s %-8s %-14s %s", code, id, tier, category, Lint.rule_class(id).reason)
|
|
103
|
+
end
|
|
104
|
+
0
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Include paths and the names of the files to lint. With -I, a path on
|
|
108
|
+
# disk is named relative to the include path above it. Without, a file's
|
|
109
|
+
# directory is its include path, and a directory is the include path of
|
|
110
|
+
# the files under it.
|
|
111
|
+
def targets(inputs, include, config)
|
|
112
|
+
roots = []
|
|
113
|
+
names = []
|
|
114
|
+
inputs.each do |input|
|
|
115
|
+
raise Error, "#{input}: No such file or directory" unless ::File.exist?(input)
|
|
116
|
+
files = if ::File.directory?(input)
|
|
117
|
+
Dir.glob("**/*.proto", base: input).sort.map { |f| ::File.join(input, f) }.reject { |f| config.ignored?(f) }
|
|
118
|
+
else
|
|
119
|
+
[input]
|
|
120
|
+
end
|
|
121
|
+
files.each do |file|
|
|
122
|
+
if include
|
|
123
|
+
name = virtual(file, include)
|
|
124
|
+
raise Error, "#{file}: not under any include path (-I)" unless name
|
|
125
|
+
else
|
|
126
|
+
root = ::File.directory?(input) ? input : ::File.dirname(input)
|
|
127
|
+
roots << root
|
|
128
|
+
name = Compiler.canonical_path(::File.expand_path(file).delete_prefix("#{::File.expand_path(root)}/"))
|
|
129
|
+
end
|
|
130
|
+
names << name
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
paths = include || roots.uniq { |r| ::File.expand_path(r) }
|
|
134
|
+
[paths, names.uniq]
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def virtual(file, include)
|
|
138
|
+
path = ::File.expand_path(file)
|
|
139
|
+
include.each do |dir|
|
|
140
|
+
root = ::File.expand_path(dir)
|
|
141
|
+
return path.delete_prefix("#{root}/") if path.start_with?("#{root}/")
|
|
142
|
+
end
|
|
143
|
+
nil
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# The diff goes to stdout with text output, to stderr when stdout is JSON
|
|
147
|
+
# or annotations
|
|
148
|
+
def fix(result, runner, write:, show:, diff_io: nil)
|
|
149
|
+
return false unless show
|
|
150
|
+
changes = Fixer.plan(result.diagnostics, text: ->(n) { runner.text(n) },
|
|
151
|
+
path: ->(n) { result.contexts.find { |c| c.name == n }&.disk_path })
|
|
152
|
+
changes.each do |change|
|
|
153
|
+
(diff_io || @out).write(Fixer.diff(change))
|
|
154
|
+
::File.binwrite(change.path, change.after) if write && change.path
|
|
155
|
+
end
|
|
156
|
+
write && !changes.empty?
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def show(diagnostics, runner, format, opts, result)
|
|
160
|
+
sources = Hash.new { |h, name| h[name] = (text = runner.text(name)) && Diagnostics::Source.new(text) }
|
|
161
|
+
case format
|
|
162
|
+
when "json"
|
|
163
|
+
diagnostics.each { |d| @out.puts Diagnostics::Formats.json(d, source: sources[d.file]) }
|
|
164
|
+
when "github"
|
|
165
|
+
diagnostics.each { |d| @out.puts Diagnostics::Formats.github(d, source: sources[d.file]) }
|
|
166
|
+
else
|
|
167
|
+
renderer = Diagnostics::Renderer.new(color: color?(opts[:color]), source: ->(d) { runner.text(d.file) })
|
|
168
|
+
diagnostics.each { |d| @out.puts renderer.render(d) }
|
|
169
|
+
@out.puts summary(diagnostics, result)
|
|
170
|
+
@out.puts EXPLAIN_HINT unless diagnostics.empty?
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def summary(diagnostics, result)
|
|
175
|
+
errors = diagnostics.count(&:error?)
|
|
176
|
+
warnings = diagnostics.count(&:warning?)
|
|
177
|
+
fixable = diagnostics.count(&:fix)
|
|
178
|
+
files = result.names.size
|
|
179
|
+
text = "#{files} #{(files == 1) ? "file" : "files"}: #{count(errors, "error")}, #{count(warnings, "warning")}"
|
|
180
|
+
text += " (#{fixable} fixable with --fix)" if fixable > 0
|
|
181
|
+
text
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def count(n, word)
|
|
185
|
+
"#{n} #{word}#{"s" unless n == 1}"
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def color?(setting)
|
|
189
|
+
case setting
|
|
190
|
+
when "always" then true
|
|
191
|
+
when "never" then false
|
|
192
|
+
else CLI.color?(@out)
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protostar
|
|
4
|
+
module Lint
|
|
5
|
+
# The `lint:` section of `.protostar.yml`:
|
|
6
|
+
#
|
|
7
|
+
# lint:
|
|
8
|
+
# rules: [default] # sets, categories or rule ids
|
|
9
|
+
# except: [PACKAGE_VERSION_SUFFIX]
|
|
10
|
+
# ignore: ["vendor/**"] # relative to the file's directory
|
|
11
|
+
# allow_comment_directives: true
|
|
12
|
+
class Config
|
|
13
|
+
FILE_NAME = ".protostar.yml"
|
|
14
|
+
KEYS = %w[rules except ignore allow_comment_directives].freeze
|
|
15
|
+
|
|
16
|
+
# @return [Array<String>]
|
|
17
|
+
attr_reader :rules
|
|
18
|
+
# @return [Array<String>]
|
|
19
|
+
attr_reader :except
|
|
20
|
+
# @return [Array<String>] globs
|
|
21
|
+
attr_reader :ignore
|
|
22
|
+
# @return [String] the directory `ignore` globs are relative to
|
|
23
|
+
attr_reader :root
|
|
24
|
+
# @return [String, nil] the file read, nil for the defaults
|
|
25
|
+
attr_reader :file
|
|
26
|
+
|
|
27
|
+
# The first `.protostar.yml` next to an input (a directory or a file's
|
|
28
|
+
# directory), else in the current directory, else the defaults
|
|
29
|
+
# @param inputs [Array<String>]
|
|
30
|
+
# @return [Config]
|
|
31
|
+
def self.find(inputs, cwd: Dir.pwd)
|
|
32
|
+
dirs = inputs.map { |i| ::File.directory?(i) ? i : ::File.dirname(i) } + [cwd]
|
|
33
|
+
dirs.each do |dir|
|
|
34
|
+
path = ::File.join(dir, FILE_NAME)
|
|
35
|
+
return load(path) if ::File.file?(path)
|
|
36
|
+
end
|
|
37
|
+
new({}, root: cwd)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @param path [String]
|
|
41
|
+
# @return [Config]
|
|
42
|
+
# @raise [Error] when it is not YAML, or the `lint:` section has an unknown key
|
|
43
|
+
def self.load(path)
|
|
44
|
+
require "yaml"
|
|
45
|
+
data = YAML.safe_load_file(path) || {}
|
|
46
|
+
raise Error, "#{path}: expected a mapping" unless data.is_a?(Hash)
|
|
47
|
+
section = data.fetch("lint", {}) || {}
|
|
48
|
+
raise Error, "#{path}: `lint` must be a mapping" unless section.is_a?(Hash)
|
|
49
|
+
unknown = section.keys - KEYS
|
|
50
|
+
raise Error, "#{path}: unknown lint setting #{unknown.join(", ")} (known: #{KEYS.join(", ")})" unless unknown.empty?
|
|
51
|
+
new(section, root: ::File.dirname(::File.expand_path(path)), file: path)
|
|
52
|
+
rescue Psych::Exception => e
|
|
53
|
+
raise Error, "#{path}: #{e.message}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def initialize(section, root:, file: nil)
|
|
57
|
+
@rules = list(section["rules"]) || ["default"]
|
|
58
|
+
@except = list(section["except"]) || []
|
|
59
|
+
@ignore = list(section["ignore"]) || []
|
|
60
|
+
@directives = section.fetch("allow_comment_directives", true) != false
|
|
61
|
+
@root = root
|
|
62
|
+
@file = file
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def allow_comment_directives?
|
|
66
|
+
@directives
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# @param path [String] a file found while walking a directory
|
|
70
|
+
# @return [Boolean] whether an `ignore` glob matches it or a directory above it
|
|
71
|
+
def ignored?(path)
|
|
72
|
+
return false if @ignore.empty?
|
|
73
|
+
rel = relative(::File.expand_path(path))
|
|
74
|
+
candidates = [rel]
|
|
75
|
+
dir = ::File.dirname(rel)
|
|
76
|
+
until dir == "." || dir == "/"
|
|
77
|
+
candidates << dir
|
|
78
|
+
dir = ::File.dirname(dir)
|
|
79
|
+
end
|
|
80
|
+
@ignore.any? do |glob|
|
|
81
|
+
candidates.any? { |c| ::File.fnmatch?(glob, c, ::File::FNM_PATHNAME | ::File::FNM_EXTGLOB) }
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def list(value)
|
|
88
|
+
return nil if value.nil?
|
|
89
|
+
Array(value).flat_map { |v| v.to_s.split(",") }.map(&:strip).reject(&:empty?)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def relative(path)
|
|
93
|
+
root = ::File.expand_path(@root)
|
|
94
|
+
path.start_with?("#{root}/") ? path.delete_prefix("#{root}/") : path
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protostar
|
|
4
|
+
module Lint
|
|
5
|
+
# What a rule sees of one compiled file: the frozen {Schema::File}, the
|
|
6
|
+
# AST with its recorded positions, the source locations with their
|
|
7
|
+
# comments, the text, and the compiler's warnings for it.
|
|
8
|
+
class Context
|
|
9
|
+
# Source paths of `FileDescriptorProto` fields the rules point at
|
|
10
|
+
PATHS = {syntax: [12], edition: [14], package: [2]}.freeze
|
|
11
|
+
|
|
12
|
+
# @return [String] the file name as imported
|
|
13
|
+
attr_reader :name
|
|
14
|
+
# @return [String, nil] the file on disk, as diagnostics print it
|
|
15
|
+
attr_reader :path
|
|
16
|
+
# @return [String, nil] the file on disk, absolute
|
|
17
|
+
attr_reader :disk_path
|
|
18
|
+
# @return [Schema::File]
|
|
19
|
+
attr_reader :file
|
|
20
|
+
# @return [Compiler::AST::File]
|
|
21
|
+
attr_reader :ast
|
|
22
|
+
# @return [String] the file's bytes
|
|
23
|
+
attr_reader :text
|
|
24
|
+
# @return [Array<Compiler::Diagnostic>] the compiler's warnings for the file
|
|
25
|
+
attr_reader :warnings
|
|
26
|
+
# @return [Array<Context>] every file of the lint run, this one included
|
|
27
|
+
attr_reader :all
|
|
28
|
+
|
|
29
|
+
# @param linker [Compiler::Linker] the linker that built the file
|
|
30
|
+
def initialize(name:, path:, disk_path:, linker:, text:, warnings:, all:)
|
|
31
|
+
@name = name
|
|
32
|
+
@path = path
|
|
33
|
+
@disk_path = disk_path
|
|
34
|
+
@linker = linker
|
|
35
|
+
@file = linker.file
|
|
36
|
+
@ast = linker.ast
|
|
37
|
+
@text = text
|
|
38
|
+
@warnings = warnings
|
|
39
|
+
@all = all
|
|
40
|
+
@extents = {}
|
|
41
|
+
@ast.locations.each do |loc|
|
|
42
|
+
span = loc.span
|
|
43
|
+
next if span.size < 3
|
|
44
|
+
finish = (span.size == 4) ? [span[2], span[3]] : [span[0], span[2]]
|
|
45
|
+
key = [span[0], span[1]]
|
|
46
|
+
@extents[key] = finish if @extents[key].nil? || (finish <=> @extents[key]) < 0
|
|
47
|
+
end
|
|
48
|
+
@by_path = {}
|
|
49
|
+
@linker.locations.each { |loc| @by_path[loc.path] ||= loc }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @return [Diagnostics::Source]
|
|
53
|
+
def source
|
|
54
|
+
@source ||= Diagnostics::Source.new(@text)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Every element of a kind, as protoc visits them, without the map entry
|
|
58
|
+
# messages and synthetic oneofs the compiler made up
|
|
59
|
+
# @param kind [Symbol] :message, :field, :oneof, :enum, :enum_value, :service or :method
|
|
60
|
+
# @yieldparam node [Compiler::AST::Node]
|
|
61
|
+
# @yieldparam descriptor [Schema::Descriptor]
|
|
62
|
+
def each(kind)
|
|
63
|
+
@linker.each_element do |node, k|
|
|
64
|
+
next unless k == kind
|
|
65
|
+
descriptor = @linker.descriptor_of(node)
|
|
66
|
+
next if synthetic?(node, kind, descriptor)
|
|
67
|
+
yield node, descriptor
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# @return [Schema::Descriptor] what the compiler built for a node
|
|
72
|
+
def descriptor(node)
|
|
73
|
+
@linker.descriptor_of(node)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# @param node [Compiler::AST::Node]
|
|
77
|
+
# @param kind [Symbol] a position kind the parser recorded: :name, :number, :type, ...
|
|
78
|
+
# @return [Diagnostics::Span, nil] from the position to the end of the element that starts there
|
|
79
|
+
def span(node, kind, label: nil, primary: true)
|
|
80
|
+
line, column = node.position(kind)
|
|
81
|
+
# oneofs record no position; every element's name is field 1 of its descriptor
|
|
82
|
+
return span_at(node.path + [1], label: label, primary: primary) if line.nil? && kind == :name && node.path
|
|
83
|
+
return nil unless line
|
|
84
|
+
end_line, end_column = @extents[[line, column]]
|
|
85
|
+
Diagnostics::Span.new(line + 1, column + 1, end_line && end_line + 1, end_column && end_column + 1,
|
|
86
|
+
label: label, primary: primary)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# @param path [Array<Integer>] a source path, e.g. [3, 0] for the first import
|
|
90
|
+
# @return [Diagnostics::Span, nil] the span of the first location with that path
|
|
91
|
+
def span_at(path, label: nil, primary: true)
|
|
92
|
+
loc = @by_path[path]
|
|
93
|
+
return nil unless loc
|
|
94
|
+
s = loc.span
|
|
95
|
+
end_line = (s.size == 4) ? s[2] : s[0]
|
|
96
|
+
Diagnostics::Span.new(s[0] + 1, s[1] + 1, end_line + 1, s.last + 1, label: label, primary: primary)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# @return [String, nil] the leading comment of an element, or its trailing
|
|
100
|
+
# one, stripped; nil when it has none
|
|
101
|
+
def comment(node)
|
|
102
|
+
loc = node.path && @by_path[node.path]
|
|
103
|
+
return nil unless loc
|
|
104
|
+
[loc.leading_comments, loc.trailing_comments].compact.map(&:strip).find { |c| !c.empty? }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Whether renaming `from` to `to` is safe to do mechanically: `from` is
|
|
108
|
+
# written once in the file, as the declaration, and `to` nowhere
|
|
109
|
+
def renamable?(from, to)
|
|
110
|
+
@words ||= @text.b.scan(/[A-Za-z_][A-Za-z0-9_]*/).tally
|
|
111
|
+
@words[from.b] == 1 && !@words.key?(to.b)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
private
|
|
115
|
+
|
|
116
|
+
def synthetic?(node, kind, descriptor)
|
|
117
|
+
case kind
|
|
118
|
+
when :message then descriptor.map_entry?
|
|
119
|
+
when :oneof then descriptor.synthetic?
|
|
120
|
+
when :field then descriptor.containing_message&.map_entry? || false
|
|
121
|
+
else false
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protostar
|
|
4
|
+
module Lint
|
|
5
|
+
# Comments that switch rules off for part of a file:
|
|
6
|
+
#
|
|
7
|
+
# // protostar-lint: disable-next-line FIELD_NOT_REQUIRED
|
|
8
|
+
# required string id = 1;
|
|
9
|
+
#
|
|
10
|
+
# // protostar-lint: disable COMMENT_FIELD, COMMENT_MESSAGE
|
|
11
|
+
# ...
|
|
12
|
+
# // protostar-lint: enable COMMENT_FIELD, COMMENT_MESSAGE
|
|
13
|
+
#
|
|
14
|
+
# A directive names rule ids or codes (a compiler warning's code works
|
|
15
|
+
# too), or none for every rule. `disable` without `enable` runs to the end
|
|
16
|
+
# of the file. Directives are read from the text line by line.
|
|
17
|
+
class Directives
|
|
18
|
+
PATTERN = %r{//\s*protostar-lint:\s*(disable-next-line|disable|enable)\b([^\n]*)}
|
|
19
|
+
|
|
20
|
+
# @param text [String] the file's bytes
|
|
21
|
+
def initialize(text)
|
|
22
|
+
@next_line = Hash.new { |h, k| h[k] = [] }
|
|
23
|
+
@ranges = []
|
|
24
|
+
open = {}
|
|
25
|
+
lines = text.b.split("\n", -1)
|
|
26
|
+
lines.each_with_index do |line, i|
|
|
27
|
+
match = PATTERN.match(line)
|
|
28
|
+
next unless match
|
|
29
|
+
names = match[2].split(/[\s,]+/).reject(&:empty?).map(&:upcase)
|
|
30
|
+
names = [:all] if names.empty?
|
|
31
|
+
number = i + 1
|
|
32
|
+
case match[1]
|
|
33
|
+
when "disable-next-line" then names.each { |n| @next_line[number + 1] << n }
|
|
34
|
+
when "disable" then names.each { |n| open[n] ||= number }
|
|
35
|
+
when "enable"
|
|
36
|
+
((names == [:all]) ? open.keys : names).each do |n|
|
|
37
|
+
start = open.delete(n) || open.delete(:all)
|
|
38
|
+
@ranges << [n, start, number] if start
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
open.each { |n, start| @ranges << [n, start, lines.size + 1] }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @param diagnostic [Compiler::Diagnostic]
|
|
46
|
+
# @return [Boolean] whether a directive switches it off where it starts
|
|
47
|
+
def suppressed?(diagnostic)
|
|
48
|
+
line = diagnostic.line
|
|
49
|
+
return false unless line
|
|
50
|
+
keys = [diagnostic.rule, diagnostic.code, :all].compact
|
|
51
|
+
return true if @next_line.key?(line) && @next_line[line].any? { |n| keys.include?(n) }
|
|
52
|
+
@ranges.any? { |n, from, to| keys.include?(n) && line > from && line < to }
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protostar
|
|
4
|
+
module Lint
|
|
5
|
+
# Applies the {Diagnostics::Fix}es lint findings carry, file by file. An
|
|
6
|
+
# edit that overlaps one already taken is left out (the finding stays
|
|
7
|
+
# unfixed), so the result never depends on two fixes agreeing.
|
|
8
|
+
class Fixer
|
|
9
|
+
CONTEXT = 3
|
|
10
|
+
|
|
11
|
+
# The fixes for one file
|
|
12
|
+
class Change
|
|
13
|
+
# @return [String] the file name as imported
|
|
14
|
+
attr_reader :name
|
|
15
|
+
# @return [String, nil] where it is on disk
|
|
16
|
+
attr_reader :path
|
|
17
|
+
# @return [String]
|
|
18
|
+
attr_reader :before
|
|
19
|
+
# @return [String]
|
|
20
|
+
attr_reader :after
|
|
21
|
+
# @return [Array<Compiler::Diagnostic>] the findings fixed
|
|
22
|
+
attr_reader :fixed
|
|
23
|
+
|
|
24
|
+
def initialize(name, path, before, after, fixed)
|
|
25
|
+
@name = name
|
|
26
|
+
@path = path
|
|
27
|
+
@before = before
|
|
28
|
+
@after = after
|
|
29
|
+
@fixed = fixed.freeze
|
|
30
|
+
freeze
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @param diagnostics [Array<Compiler::Diagnostic>]
|
|
35
|
+
# @param text [#call] the text of a file, by name
|
|
36
|
+
# @param path [#call] where a file is on disk, by name, or nil
|
|
37
|
+
# @return [Array<Change>] one per file with a fix
|
|
38
|
+
def self.plan(diagnostics, text:, path:)
|
|
39
|
+
diagnostics.select(&:fix).group_by(&:file).filter_map do |name, found|
|
|
40
|
+
before = text.call(name)
|
|
41
|
+
next unless before
|
|
42
|
+
taken = []
|
|
43
|
+
fixed = found.select do |d|
|
|
44
|
+
edits = d.fix.edits
|
|
45
|
+
next false if edits.any? { |e| taken.any? { |t| overlap?(e, t) } }
|
|
46
|
+
taken.concat(edits)
|
|
47
|
+
true
|
|
48
|
+
end
|
|
49
|
+
after = Diagnostics::Source.new(before).apply(before, taken)
|
|
50
|
+
Change.new(name, path.call(name), before, after, fixed)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self.overlap?(a, b)
|
|
55
|
+
a_start = [a.line, a.column]
|
|
56
|
+
a_end = [a.end_line, a.end_column]
|
|
57
|
+
b_start = [b.line, b.column]
|
|
58
|
+
b_end = [b.end_line, b.end_column]
|
|
59
|
+
return a_start == b_start if a_start == a_end || b_start == b_end
|
|
60
|
+
(a_start <=> b_end) < 0 && (b_start <=> a_end) < 0
|
|
61
|
+
end
|
|
62
|
+
private_class_method :overlap?
|
|
63
|
+
|
|
64
|
+
# @param change [Change]
|
|
65
|
+
# @return [String] a unified diff, as `diff -u` prints it
|
|
66
|
+
def self.diff(change)
|
|
67
|
+
old_lines = change.before.split(/(?<=\n)/)
|
|
68
|
+
new_lines = change.after.split(/(?<=\n)/)
|
|
69
|
+
label = change.path || change.name
|
|
70
|
+
old_label, new_label = label.start_with?("/") ? [label, label] : ["a/#{label}", "b/#{label}"]
|
|
71
|
+
out = "--- #{old_label}\n+++ #{new_label}\n"
|
|
72
|
+
hunks(old_lines, new_lines).each do |os, oe, ns, ne|
|
|
73
|
+
out << "@@ -#{range(os, oe)} +#{range(ns, ne)} @@\n"
|
|
74
|
+
i = os
|
|
75
|
+
j = ns
|
|
76
|
+
while i < oe || j < ne
|
|
77
|
+
if i < oe && j < ne && old_lines[i] == new_lines[j]
|
|
78
|
+
out << " " << terminated(old_lines[i])
|
|
79
|
+
i += 1
|
|
80
|
+
j += 1
|
|
81
|
+
else
|
|
82
|
+
changed_old, changed_new = block(old_lines, new_lines, i, j, oe, ne)
|
|
83
|
+
changed_old.each { |l| out << "-" << terminated(l) }
|
|
84
|
+
changed_new.each { |l| out << "+" << terminated(l) }
|
|
85
|
+
i += changed_old.size
|
|
86
|
+
j += changed_new.size
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
out
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# The lines that differ, as aligned runs: lint's edits change lines in
|
|
94
|
+
# place or insert whole lines, so a run ends where the two sides meet again
|
|
95
|
+
def self.block(old_lines, new_lines, i, j, oe, ne)
|
|
96
|
+
(0..[oe - i, ne - j].max).each do |skip|
|
|
97
|
+
(0..skip).each do |a|
|
|
98
|
+
b = skip - a
|
|
99
|
+
next if i + a > oe || j + b > ne
|
|
100
|
+
if (i + a == oe && j + b == ne) || (i + a < oe && j + b < ne && old_lines[i + a] == new_lines[j + b])
|
|
101
|
+
return [old_lines[i, a], new_lines[j, b]] if a + b > 0
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
[old_lines[i...oe], new_lines[j...ne]]
|
|
106
|
+
end
|
|
107
|
+
private_class_method :block
|
|
108
|
+
|
|
109
|
+
# Hunks as [old start, old end, new start, new end], 0-based, ends exclusive
|
|
110
|
+
def self.hunks(old_lines, new_lines)
|
|
111
|
+
prefix = 0
|
|
112
|
+
prefix += 1 while prefix < old_lines.size && prefix < new_lines.size && old_lines[prefix] == new_lines[prefix]
|
|
113
|
+
suffix = 0
|
|
114
|
+
while suffix < old_lines.size - prefix && suffix < new_lines.size - prefix &&
|
|
115
|
+
old_lines[-1 - suffix] == new_lines[-1 - suffix]
|
|
116
|
+
suffix += 1
|
|
117
|
+
end
|
|
118
|
+
return [] if prefix == old_lines.size && prefix == new_lines.size
|
|
119
|
+
changes = []
|
|
120
|
+
i = prefix
|
|
121
|
+
j = prefix
|
|
122
|
+
oe = old_lines.size - suffix
|
|
123
|
+
ne = new_lines.size - suffix
|
|
124
|
+
while i < oe || j < ne
|
|
125
|
+
if i < oe && j < ne && old_lines[i] == new_lines[j]
|
|
126
|
+
i += 1
|
|
127
|
+
j += 1
|
|
128
|
+
next
|
|
129
|
+
end
|
|
130
|
+
a, b = block(old_lines, new_lines, i, j, oe, ne)
|
|
131
|
+
changes << [i, i + a.size, j, j + b.size]
|
|
132
|
+
i += a.size
|
|
133
|
+
j += b.size
|
|
134
|
+
end
|
|
135
|
+
merged = []
|
|
136
|
+
changes.each do |os, oe2, ns, ne2|
|
|
137
|
+
if merged.any? && os - merged.last[1] <= 2 * CONTEXT
|
|
138
|
+
merged.last[1] = oe2
|
|
139
|
+
merged.last[3] = ne2
|
|
140
|
+
else
|
|
141
|
+
merged << [os, oe2, ns, ne2]
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
merged.map do |os, oe2, ns, ne2|
|
|
145
|
+
before = [CONTEXT, os].min
|
|
146
|
+
after = [CONTEXT, old_lines.size - oe2].min
|
|
147
|
+
[os - before, oe2 + after, ns - before, ne2 + after]
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
private_class_method :hunks
|
|
151
|
+
|
|
152
|
+
def self.range(start, finish)
|
|
153
|
+
count = finish - start
|
|
154
|
+
first = count.zero? ? start : start + 1
|
|
155
|
+
(count == 1) ? first.to_s : "#{first},#{count}"
|
|
156
|
+
end
|
|
157
|
+
private_class_method :range
|
|
158
|
+
|
|
159
|
+
def self.terminated(line)
|
|
160
|
+
line.end_with?("\n") ? line : "#{line}\n\\n"
|
|
161
|
+
end
|
|
162
|
+
private_class_method :terminated
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|