rigortype 0.1.19 → 0.2.1
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 +4 -4
- data/README.md +41 -6
- data/data/core_overlay/numeric.rbs +33 -0
- data/data/core_overlay/pathname.rbs +25 -0
- data/data/core_overlay/string_scanner.rbs +28 -0
- data/data/gem_overlay/activesupport/core_ext.rbs +473 -0
- data/data/vendored_gem_sigs/ast/ast.rbs +130 -0
- data/data/vendored_gem_sigs/bcrypt/bcrypt.rbs +47 -0
- data/data/vendored_gem_sigs/bundler/bundler.rbs +238 -0
- data/data/vendored_gem_sigs/cgi/cgi_extras.rbs +34 -0
- data/data/vendored_gem_sigs/did_you_mean/did_you_mean_extras.rbs +34 -0
- data/data/vendored_gem_sigs/idn-ruby/idn.rbs +54 -0
- data/data/vendored_gem_sigs/mysql2/client.rbs +55 -0
- data/data/vendored_gem_sigs/mysql2/error.rbs +5 -0
- data/data/vendored_gem_sigs/mysql2/result.rbs +31 -0
- data/data/vendored_gem_sigs/mysql2/statement.rbs +5 -0
- data/data/vendored_gem_sigs/nokogiri/nokogiri.rbs +2332 -0
- data/data/vendored_gem_sigs/nokogiri/nokogiri_html5.rbs +47 -0
- data/data/vendored_gem_sigs/pg/pg.rbs +212 -0
- data/data/vendored_gem_sigs/prism/prism_supplement.rbs +44 -0
- data/data/vendored_gem_sigs/redis/errors.rbs +50 -0
- data/data/vendored_gem_sigs/redis/future.rbs +5 -0
- data/data/vendored_gem_sigs/redis/redis.rbs +348 -0
- data/data/vendored_gem_sigs/redis/redis_extras.rbs +130 -0
- data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +226 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -23
- data/lib/rigor/analysis/check_rules/rule_walk.rb +3 -21
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +24 -15
- data/lib/rigor/analysis/check_rules.rb +492 -71
- data/lib/rigor/analysis/dependency_source_inference/index.rb +4 -7
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -18
- data/lib/rigor/analysis/dependency_source_inference.rb +3 -12
- data/lib/rigor/analysis/fact_store.rb +5 -4
- data/lib/rigor/analysis/rule_catalog.rb +153 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +17 -17
- data/lib/rigor/analysis/runner/project_pre_passes.rb +9 -8
- data/lib/rigor/analysis/runner.rb +17 -6
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +3 -4
- data/lib/rigor/analysis/worker_session.rb +10 -14
- data/lib/rigor/builtins/predefined_constant_refinements.rb +151 -0
- data/lib/rigor/cache/store.rb +5 -3
- data/lib/rigor/cli/annotate_command.rb +28 -7
- data/lib/rigor/cli/baseline_command.rb +4 -3
- data/lib/rigor/cli/check_command.rb +138 -16
- data/lib/rigor/cli/coverage_command.rb +138 -31
- data/lib/rigor/cli/coverage_mutation.rb +149 -0
- data/lib/rigor/cli/coverage_scan.rb +57 -0
- data/lib/rigor/cli/explain_command.rb +2 -0
- data/lib/rigor/cli/fused_protection_renderer.rb +67 -0
- data/lib/rigor/cli/fused_protection_report.rb +76 -0
- data/lib/rigor/cli/lsp_command.rb +3 -7
- data/lib/rigor/cli/mutation_protection_renderer.rb +63 -0
- data/lib/rigor/cli/mutation_protection_report.rb +73 -0
- data/lib/rigor/cli/options.rb +9 -0
- data/lib/rigor/cli/plugins_command.rb +2 -1
- data/lib/rigor/cli/protection_renderer.rb +63 -0
- data/lib/rigor/cli/protection_report.rb +68 -0
- data/lib/rigor/cli/sig_gen_command.rb +2 -1
- data/lib/rigor/cli/trace_command.rb +2 -1
- data/lib/rigor/cli/triage_command.rb +2 -1
- data/lib/rigor/cli/type_of_command.rb +1 -1
- data/lib/rigor/cli/type_scan_command.rb +2 -1
- data/lib/rigor/cli.rb +3 -2
- data/lib/rigor/config_audit.rb +152 -0
- data/lib/rigor/configuration/dependencies.rb +2 -4
- data/lib/rigor/configuration.rb +57 -7
- data/lib/rigor/environment/bundle_sig_discovery.rb +61 -13
- data/lib/rigor/environment/class_registry.rb +4 -3
- data/lib/rigor/environment/constant_type_cache_holder.rb +43 -0
- data/lib/rigor/environment/lockfile_resolver.rb +1 -1
- data/lib/rigor/environment/rbs_collection_discovery.rb +1 -2
- data/lib/rigor/environment/rbs_coverage_report.rb +2 -1
- data/lib/rigor/environment/rbs_loader.rb +76 -5
- data/lib/rigor/environment.rb +66 -8
- data/lib/rigor/flow_contribution/fact.rb +1 -1
- data/lib/rigor/flow_contribution.rb +3 -5
- data/lib/rigor/inference/acceptance.rb +17 -9
- data/lib/rigor/inference/block_parameter_binder.rb +2 -3
- data/lib/rigor/inference/builtins/comparable_catalog.rb +2 -2
- data/lib/rigor/inference/builtins/enumerable_catalog.rb +2 -2
- data/lib/rigor/inference/builtins/method_catalog.rb +19 -0
- data/lib/rigor/inference/builtins/string_catalog.rb +9 -1
- data/lib/rigor/inference/expression_typer.rb +20 -28
- data/lib/rigor/inference/hkt_body.rb +8 -11
- data/lib/rigor/inference/hkt_body_parser.rb +10 -12
- data/lib/rigor/inference/hkt_registry.rb +10 -11
- data/lib/rigor/inference/method_dispatcher/call_context.rb +1 -4
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +169 -24
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +9 -73
- data/lib/rigor/inference/method_dispatcher/file_folding.rb +6 -7
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +10 -16
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +25 -13
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +93 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +1 -3
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +24 -22
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +90 -15
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +303 -0
- data/lib/rigor/inference/method_dispatcher.rb +40 -48
- data/lib/rigor/inference/mutation_widening.rb +5 -11
- data/lib/rigor/inference/narrowing.rb +14 -16
- data/lib/rigor/inference/parameter_inference_collector.rb +367 -0
- data/lib/rigor/inference/project_patched_methods.rb +4 -7
- data/lib/rigor/inference/project_patched_scanner.rb +2 -13
- data/lib/rigor/inference/protection_scanner.rb +86 -0
- data/lib/rigor/inference/scope_indexer.rb +129 -55
- data/lib/rigor/inference/statement_evaluator.rb +271 -114
- data/lib/rigor/inference/struct_fold_safety.rb +181 -0
- data/lib/rigor/inference/synthetic_method.rb +7 -7
- data/lib/rigor/language_server/completion_provider.rb +6 -12
- data/lib/rigor/language_server/diagnostic_publisher.rb +4 -4
- data/lib/rigor/language_server/document_symbol_provider.rb +3 -3
- data/lib/rigor/language_server/hover_provider.rb +2 -3
- data/lib/rigor/language_server/hover_renderer.rb +2 -11
- data/lib/rigor/language_server/server.rb +9 -17
- data/lib/rigor/language_server.rb +4 -5
- data/lib/rigor/plugin/base.rb +10 -8
- data/lib/rigor/plugin/macro/block_as_method.rb +3 -4
- data/lib/rigor/plugin/macro/heredoc_template.rb +4 -7
- data/lib/rigor/plugin/macro/trait_registry.rb +3 -6
- data/lib/rigor/plugin/macro.rb +4 -5
- data/lib/rigor/plugin/manifest.rb +45 -66
- data/lib/rigor/plugin/registry.rb +6 -7
- data/lib/rigor/plugin/type_node_resolver.rb +6 -8
- data/lib/rigor/protection/diagnostic_oracle.rb +51 -0
- data/lib/rigor/protection/mutation_scanner.rb +180 -0
- data/lib/rigor/protection/mutator.rb +267 -0
- data/lib/rigor/protection/test_suite_oracle.rb +68 -0
- data/lib/rigor/rbs_extended.rb +24 -36
- data/lib/rigor/reflection.rb +4 -7
- data/lib/rigor/scope/discovery_index.rb +14 -2
- data/lib/rigor/scope.rb +54 -11
- data/lib/rigor/sig_gen/observed_call.rb +3 -3
- data/lib/rigor/sig_gen/writer.rb +40 -2
- data/lib/rigor/signature_path_audit.rb +92 -0
- data/lib/rigor/source/constant_path.rb +62 -0
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/type/bound_method.rb +2 -11
- data/lib/rigor/type/combinator.rb +16 -3
- data/lib/rigor/type/constant.rb +2 -11
- data/lib/rigor/type/data_class.rb +2 -11
- data/lib/rigor/type/data_instance.rb +2 -11
- data/lib/rigor/type/hash_shape.rb +2 -11
- data/lib/rigor/type/integer_range.rb +2 -11
- data/lib/rigor/type/intersection.rb +2 -11
- data/lib/rigor/type/nominal.rb +2 -11
- data/lib/rigor/type/plain_lattice.rb +37 -0
- data/lib/rigor/type/refined.rb +72 -13
- data/lib/rigor/type/singleton.rb +2 -11
- data/lib/rigor/type/struct_class.rb +75 -0
- data/lib/rigor/type/struct_instance.rb +93 -0
- data/lib/rigor/type/tuple.rb +5 -15
- data/lib/rigor/type.rb +2 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +3 -3
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +3 -3
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +5 -13
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +11 -17
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +7 -10
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +3 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +6 -8
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +5 -7
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +1 -2
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +9 -11
- data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +8 -9
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +13 -12
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +3 -4
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +8 -8
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +9 -11
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +7 -8
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +7 -9
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +12 -13
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +15 -23
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +3 -3
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +3 -3
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +2 -4
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +27 -11
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +1 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +4 -6
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +12 -18
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +5 -5
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -4
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +19 -14
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +0 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +5 -4
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +2 -3
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +7 -11
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +4 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +6 -9
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +5 -15
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +28 -41
- data/sig/rigor/scope.rbs +9 -1
- data/sig/rigor/type.rbs +36 -1
- metadata +49 -1
|
@@ -1,14 +1,30 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "English"
|
|
3
4
|
require "optionparser"
|
|
4
5
|
require "prism"
|
|
6
|
+
require "shellwords"
|
|
5
7
|
|
|
6
8
|
require_relative "../configuration"
|
|
9
|
+
require_relative "options"
|
|
7
10
|
require_relative "../environment"
|
|
8
11
|
require_relative "../inference/precision_scanner"
|
|
12
|
+
require_relative "../inference/protection_scanner"
|
|
13
|
+
require_relative "../inference/parameter_inference_collector"
|
|
14
|
+
require_relative "../protection/mutation_scanner"
|
|
15
|
+
require_relative "../protection/test_suite_oracle"
|
|
16
|
+
require_relative "../language_server/project_context"
|
|
9
17
|
require_relative "../scope"
|
|
10
18
|
require_relative "coverage_report"
|
|
11
19
|
require_relative "coverage_renderer"
|
|
20
|
+
require_relative "coverage_scan"
|
|
21
|
+
require_relative "protection_report"
|
|
22
|
+
require_relative "protection_renderer"
|
|
23
|
+
require_relative "mutation_protection_report"
|
|
24
|
+
require_relative "mutation_protection_renderer"
|
|
25
|
+
require_relative "fused_protection_report"
|
|
26
|
+
require_relative "fused_protection_renderer"
|
|
27
|
+
require_relative "coverage_mutation"
|
|
12
28
|
require_relative "command"
|
|
13
29
|
|
|
14
30
|
module Rigor
|
|
@@ -27,15 +43,28 @@ module Rigor
|
|
|
27
43
|
# 1 — precision ratio < threshold, or parse errors encountered
|
|
28
44
|
# 64 — usage error
|
|
29
45
|
class CoverageCommand < Command
|
|
46
|
+
include CoverageMutation
|
|
47
|
+
|
|
30
48
|
USAGE = "Usage: rigor coverage [options] PATH..."
|
|
31
49
|
|
|
50
|
+
# ADR-70 — the default test runner hook for `--with-tests`. The
|
|
51
|
+
# conventional Ruby test task; override with `--test-command`.
|
|
52
|
+
DEFAULT_TEST_COMMAND = %w[bundle exec rake].freeze
|
|
53
|
+
|
|
32
54
|
# @return [Integer] CLI exit status.
|
|
33
55
|
def run
|
|
34
56
|
options = parse_options
|
|
57
|
+
return mutation_misuse_error if options[:mutation] && !options[:protection]
|
|
58
|
+
return with_tests_misuse_error if options[:with_tests] && !options[:mutation]
|
|
59
|
+
return include_dynamic_misuse_error if options[:include_dynamic] && !options[:with_tests]
|
|
60
|
+
return run_mutation_protection(options) if options[:mutation]
|
|
61
|
+
|
|
35
62
|
paths = collect_paths(@argv, command_name: "coverage")
|
|
36
63
|
return CLI::EXIT_USAGE if paths.nil?
|
|
37
64
|
return usage_error if paths.empty?
|
|
38
65
|
|
|
66
|
+
return run_protection(paths, options) if options[:protection]
|
|
67
|
+
|
|
39
68
|
report = scan_paths(paths, options)
|
|
40
69
|
CoverageRenderer.new(out: @out).render(report, format: options.fetch(:format))
|
|
41
70
|
determine_exit(report, options)
|
|
@@ -44,53 +73,131 @@ module Rigor
|
|
|
44
73
|
private
|
|
45
74
|
|
|
46
75
|
def parse_options
|
|
47
|
-
options = { format: "text", threshold: nil, config: nil
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
opts.on("--format=FORMAT", "Output format: text or json") { |v| options[:format] = v }
|
|
52
|
-
opts.on("--config=PATH", "Path to the Rigor configuration file") { |v| options[:config] = v }
|
|
53
|
-
opts.on(
|
|
54
|
-
"--threshold=RATIO", Float,
|
|
55
|
-
"Exit 1 when precision ratio is below RATIO (0.0–1.0)"
|
|
56
|
-
) { |v| options[:threshold] = v }
|
|
57
|
-
end.parse!(@argv)
|
|
58
|
-
|
|
76
|
+
options = { format: "text", threshold: nil, config: nil, protection: false, mutation: false,
|
|
77
|
+
with_tests: false, test_command: DEFAULT_TEST_COMMAND, include_dynamic: false,
|
|
78
|
+
limit: nil, seed: 1 }
|
|
79
|
+
OptionParser.new { |opts| define_options(opts, options) }.parse!(@argv)
|
|
59
80
|
options
|
|
60
81
|
end
|
|
61
82
|
|
|
62
|
-
def
|
|
63
|
-
|
|
83
|
+
def define_options(opts, options)
|
|
84
|
+
opts.banner = USAGE
|
|
85
|
+
opts.on("--format=FORMAT", "Output format: text or json") { |v| options[:format] = v }
|
|
86
|
+
Options.add_config(opts, options)
|
|
87
|
+
opts.on("--protection", "Report type-protection coverage (ADR-63 Tier 1) instead of type precision") do
|
|
88
|
+
options[:protection] = true
|
|
89
|
+
end
|
|
90
|
+
define_mutation_options(opts, options)
|
|
91
|
+
opts.on("--threshold=RATIO", Float, "Exit 1 when the precision (or, with --protection, " \
|
|
92
|
+
"protection/effectiveness) ratio is below RATIO (0.0–1.0)") do |v|
|
|
93
|
+
options[:threshold] = v
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def define_mutation_options(opts, options)
|
|
98
|
+
opts.on("--mutation", "With --protection: measure actual mutation effectiveness (ADR-63 Tier 2). " \
|
|
99
|
+
"Scopes to git-changed files when no paths are given; explicit paths override.") do
|
|
100
|
+
options[:mutation] = true
|
|
101
|
+
end
|
|
102
|
+
opts.on("--with-tests", "With --mutation: also measure dynamic (test-suite) protection (ADR-70). " \
|
|
103
|
+
"Runs --test-command against each type-survivor; reports the fused map.") do
|
|
104
|
+
options[:with_tests] = true
|
|
105
|
+
end
|
|
106
|
+
opts.on("--test-command=CMD", "The test runner hook for --with-tests " \
|
|
107
|
+
"(default: #{DEFAULT_TEST_COMMAND.join(' ')})") do |v|
|
|
108
|
+
options[:test_command] = Shellwords.split(v)
|
|
109
|
+
end
|
|
110
|
+
opts.on("--include-dynamic", "With --with-tests: also mutate Dynamic-receiver (untyped) sites, where a " \
|
|
111
|
+
"test is the only protection (ADR-69 Seam 2). Completes the map, runs more.") do
|
|
112
|
+
options[:include_dynamic] = true
|
|
113
|
+
end
|
|
114
|
+
opts.on("--limit=N", Integer,
|
|
115
|
+
"Sample at most N mutations/file under --mutation (caps cost; ratios become estimates)") do |v|
|
|
116
|
+
options[:limit] = v
|
|
117
|
+
end
|
|
118
|
+
opts.on("--seed=N", Integer, "RNG seed for --limit sampling (default 1)") { |v| options[:seed] = v }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def mutation_misuse_error
|
|
122
|
+
@err.puts("coverage: --mutation requires --protection")
|
|
64
123
|
@err.puts(USAGE)
|
|
65
124
|
CLI::EXIT_USAGE
|
|
66
125
|
end
|
|
67
126
|
|
|
68
|
-
def
|
|
127
|
+
def with_tests_misuse_error
|
|
128
|
+
@err.puts("coverage: --with-tests requires --mutation (and --protection)")
|
|
129
|
+
@err.puts(USAGE)
|
|
130
|
+
CLI::EXIT_USAGE
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def include_dynamic_misuse_error
|
|
134
|
+
@err.puts("coverage: --include-dynamic requires --with-tests (a Dynamic site's only protection is a test)")
|
|
135
|
+
@err.puts(USAGE)
|
|
136
|
+
CLI::EXIT_USAGE
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def run_protection(paths, options)
|
|
140
|
+
report = scan_protection(paths, options)
|
|
141
|
+
ProtectionRenderer.new(out: @out).render(report, format: options.fetch(:format))
|
|
142
|
+
determine_protection_exit(report, options)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def scan_protection(paths, options)
|
|
69
146
|
configuration = Configuration.load(options.fetch(:config))
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
147
|
+
environment = project_environment(configuration)
|
|
148
|
+
scope = scope_with_inferred_params(paths, configuration, environment)
|
|
149
|
+
scanner = Inference::ProtectionScanner.new(scope: scope)
|
|
150
|
+
accumulator = ProtectionAccumulator.new
|
|
73
151
|
|
|
74
152
|
paths.each { |path| scan_one(path, scanner, accumulator, configuration) }
|
|
75
|
-
accumulator.to_report
|
|
153
|
+
accumulator.to_report
|
|
76
154
|
end
|
|
77
155
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
156
|
+
# ADR-67 WD3 — seed the call-site parameter-inference table so the
|
|
157
|
+
# protection scan counts an inferred-parameter receiver (e.g. `node.loc`
|
|
158
|
+
# where `node` is a `def compile(node)` parameter) as protected when its
|
|
159
|
+
# call sites resolve to concrete argument types. ONLY the parameter table
|
|
160
|
+
# is seeded — no cross-file discovery — so every site that does not gain
|
|
161
|
+
# an inferred parameter type is classified byte-identically to the
|
|
162
|
+
# un-inferred baseline. Collection spans the scanned `paths`.
|
|
163
|
+
def scope_with_inferred_params(paths, configuration, environment)
|
|
164
|
+
base = Scope.empty(environment: environment)
|
|
165
|
+
table = Inference::ParameterInferenceCollector.collect(
|
|
166
|
+
files: paths, environment: environment, target_ruby: configuration.target_ruby
|
|
82
167
|
)
|
|
168
|
+
return base if table.empty?
|
|
169
|
+
|
|
170
|
+
base.with_discovery(base.discovery.with(param_inferred_types: table))
|
|
83
171
|
end
|
|
84
172
|
|
|
85
|
-
def
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
173
|
+
def determine_protection_exit(report, options)
|
|
174
|
+
return 1 unless report.parse_errors.empty?
|
|
175
|
+
|
|
176
|
+
threshold = options[:threshold]
|
|
177
|
+
return 0 if threshold.nil?
|
|
178
|
+
|
|
179
|
+
report.ratio < threshold ? 1 : 0
|
|
180
|
+
end
|
|
92
181
|
|
|
93
|
-
|
|
182
|
+
def usage_error
|
|
183
|
+
@err.puts("coverage: at least one path is required")
|
|
184
|
+
@err.puts(USAGE)
|
|
185
|
+
CLI::EXIT_USAGE
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def scan_paths(paths, options)
|
|
189
|
+
CoverageScan.precision_report(files: paths, configuration: Configuration.load(options.fetch(:config)))
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Delegated to the shared scan module (see {CoverageScan}); the
|
|
193
|
+
# protection path below reuses both, and `rigor check --coverage`
|
|
194
|
+
# reuses `precision_report` over the same machinery.
|
|
195
|
+
def project_environment(configuration)
|
|
196
|
+
CoverageScan.project_environment(configuration)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def scan_one(path, scanner, accumulator, configuration)
|
|
200
|
+
CoverageScan.scan_into(path, scanner, accumulator, configuration)
|
|
94
201
|
end
|
|
95
202
|
|
|
96
203
|
def determine_exit(report, options)
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "English"
|
|
4
|
+
require "prism"
|
|
5
|
+
|
|
6
|
+
module Rigor
|
|
7
|
+
class CLI
|
|
8
|
+
# ADR-63 Tier 2 + ADR-70 — the mutation-effectiveness and fused static∪dynamic
|
|
9
|
+
# protection paths, factored out of {CoverageCommand} to keep that command
|
|
10
|
+
# focused on dispatch. Mixed in, so each method runs in the command instance
|
|
11
|
+
# (using `@out` / `@err` / `@argv` / `collect_paths` / `determine_protection_exit`
|
|
12
|
+
# and the Protection + LanguageServer collaborators the command requires).
|
|
13
|
+
module CoverageMutation
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
# ADR-63 Tier 2 — the mutation-effectiveness deep dive. Builds the RBS
|
|
17
|
+
# environment + project pre-pass once (the warm loop), then re-analyses
|
|
18
|
+
# each target file's mutants against its clean baseline. Defaults to the
|
|
19
|
+
# git-changed `.rb` files; explicit paths override (and enable the
|
|
20
|
+
# whole-project opt-in, which is minutes).
|
|
21
|
+
def run_mutation_protection(options)
|
|
22
|
+
explicit = collect_paths(@argv, command_name: "coverage")
|
|
23
|
+
return CLI::EXIT_USAGE if explicit.nil?
|
|
24
|
+
|
|
25
|
+
target_files = explicit.empty? ? changed_ruby_files : explicit
|
|
26
|
+
if target_files.empty?
|
|
27
|
+
@out.puts("No changed Ruby files to measure — pass paths to measure explicitly.")
|
|
28
|
+
return 0
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
note_sampling(options)
|
|
32
|
+
return run_fused_protection(target_files, options) if options[:with_tests]
|
|
33
|
+
|
|
34
|
+
report = scan_mutation_protection(target_files, options)
|
|
35
|
+
MutationProtectionRenderer.new(out: @out).render(report, format: options.fetch(:format))
|
|
36
|
+
determine_protection_exit(report, options)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# A `--limit` sample makes the report an estimate (per-file ratios over a
|
|
40
|
+
# random N of the mutations). Say so on stderr — stdout stays clean for JSON.
|
|
41
|
+
def note_sampling(options)
|
|
42
|
+
return unless options[:limit]
|
|
43
|
+
|
|
44
|
+
@err.puts(
|
|
45
|
+
"coverage: sampling at most #{options[:limit]} mutations/file " \
|
|
46
|
+
"(seed #{options[:seed]}); ratios are estimates."
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# ADR-70 — the fused static∪dynamic deep dive. The type pass is the ADR-63
|
|
51
|
+
# Tier 2 warm loop; each type-survivor is then run against the project's
|
|
52
|
+
# test suite (the runner hook). The suite MUST pass on clean code first, or
|
|
53
|
+
# "a mutant survived" is meaningless — abort with a clear message if not.
|
|
54
|
+
def run_fused_protection(paths, options)
|
|
55
|
+
configuration = Configuration.load(options.fetch(:config))
|
|
56
|
+
test_oracle = Protection::TestSuiteOracle.new(command: options.fetch(:test_command))
|
|
57
|
+
return suite_not_green_error(options) unless test_oracle.green?
|
|
58
|
+
|
|
59
|
+
context = LanguageServer::ProjectContext.new(configuration: configuration)
|
|
60
|
+
scanner = Protection::MutationScanner.new(
|
|
61
|
+
configuration: configuration, environment: context.environment, project_scan: context.project_scan,
|
|
62
|
+
limit: options[:limit], seed: options[:seed],
|
|
63
|
+
site_selector: options[:include_dynamic] ? :all : :biteable
|
|
64
|
+
)
|
|
65
|
+
accumulator = FusedProtectionAccumulator.new
|
|
66
|
+
paths.each { |path| scan_fused_one(path, scanner, accumulator, test_oracle, configuration) }
|
|
67
|
+
report = accumulator.to_report
|
|
68
|
+
FusedProtectionRenderer.new(out: @out).render(report, format: options.fetch(:format))
|
|
69
|
+
determine_protection_exit(report, options)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def scan_fused_one(path, scanner, accumulator, test_oracle, configuration)
|
|
73
|
+
source = File.read(path)
|
|
74
|
+
parse_result = Prism.parse(source, filepath: path, version: configuration.target_ruby)
|
|
75
|
+
if parse_result.errors.any?
|
|
76
|
+
accumulator.record_parse_error(path, parse_result.errors)
|
|
77
|
+
return
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
accumulator.absorb(scanner.scan_file_fused(path, source: source, test_oracle: test_oracle))
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def suite_not_green_error(options)
|
|
84
|
+
@err.puts(
|
|
85
|
+
"coverage: the test suite must pass on clean code to measure test protection " \
|
|
86
|
+
"(ran: #{options.fetch(:test_command).join(' ')})"
|
|
87
|
+
)
|
|
88
|
+
@err.puts(
|
|
89
|
+
" the command must exit 0 on a clean tree. A non-zero exit on otherwise-passing " \
|
|
90
|
+
"tests also trips this — e.g. a SimpleCov coverage floor on a file-scoped run; " \
|
|
91
|
+
"point --test-command at a plain pass/fail runner."
|
|
92
|
+
)
|
|
93
|
+
1
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def scan_mutation_protection(paths, options)
|
|
97
|
+
configuration = Configuration.load(options.fetch(:config))
|
|
98
|
+
context = LanguageServer::ProjectContext.new(configuration: configuration)
|
|
99
|
+
scanner = Protection::MutationScanner.new(
|
|
100
|
+
configuration: configuration, environment: context.environment, project_scan: context.project_scan,
|
|
101
|
+
limit: options[:limit], seed: options[:seed]
|
|
102
|
+
)
|
|
103
|
+
accumulator = MutationProtectionAccumulator.new
|
|
104
|
+
|
|
105
|
+
paths.each { |path| scan_mutation_one(path, scanner, accumulator, configuration) }
|
|
106
|
+
accumulator.to_report
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def scan_mutation_one(path, scanner, accumulator, configuration)
|
|
110
|
+
source = File.read(path)
|
|
111
|
+
parse_result = Prism.parse(source, filepath: path, version: configuration.target_ruby)
|
|
112
|
+
if parse_result.errors.any?
|
|
113
|
+
accumulator.record_parse_error(path, parse_result.errors)
|
|
114
|
+
return
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
accumulator.absorb(scanner.scan_file(path, source: source))
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# The git-changed (modified / added / untracked) `.rb` files that exist on
|
|
121
|
+
# disk — the default Tier 2 scope. Returns [] outside a git work tree or
|
|
122
|
+
# when git is unavailable; the caller then reports "nothing to measure".
|
|
123
|
+
def changed_ruby_files
|
|
124
|
+
output = git_status_porcelain
|
|
125
|
+
return [] if output.nil?
|
|
126
|
+
|
|
127
|
+
output.each_line.filter_map { |line| changed_path(line) }.uniq.select { |p| File.file?(p) }
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Parse one `git status --porcelain` line (`XY <path>`, or `R old -> new`)
|
|
131
|
+
# into a candidate `.rb` path, or nil.
|
|
132
|
+
def changed_path(line)
|
|
133
|
+
path = line[3..]&.chomp
|
|
134
|
+
return nil if path.nil? || path.empty?
|
|
135
|
+
|
|
136
|
+
path = path.split(" -> ", 2).last if path.include?(" -> ")
|
|
137
|
+
path = path.delete_prefix('"').delete_suffix('"')
|
|
138
|
+
path.end_with?(".rb") ? path : nil
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def git_status_porcelain
|
|
142
|
+
output = IO.popen(%w[git status --porcelain --untracked-files=all], err: File::NULL, &:read)
|
|
143
|
+
$CHILD_STATUS&.success? ? output : nil
|
|
144
|
+
rescue SystemCallError
|
|
145
|
+
nil
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../configuration"
|
|
6
|
+
require_relative "../environment"
|
|
7
|
+
require_relative "../inference/precision_scanner"
|
|
8
|
+
require_relative "../scope"
|
|
9
|
+
require_relative "coverage_report"
|
|
10
|
+
|
|
11
|
+
module Rigor
|
|
12
|
+
class CLI
|
|
13
|
+
# Shared type-precision scan behind both `rigor coverage` (the
|
|
14
|
+
# dedicated command) and `rigor check --coverage` (the in-run
|
|
15
|
+
# coverage block). Walks each file's AST, types every expression via
|
|
16
|
+
# `Scope#type_of`, and accumulates the precision-tier breakdown into a
|
|
17
|
+
# `CoverageReport`. Extracted so the two surfaces stay byte-identical
|
|
18
|
+
# on the same file set.
|
|
19
|
+
module CoverageScan
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
# @param files [Array<String>] explicit `.rb` file paths to scan.
|
|
23
|
+
# @param configuration [Rigor::Configuration]
|
|
24
|
+
# @return [Rigor::CLI::CoverageReport]
|
|
25
|
+
def precision_report(files:, configuration:)
|
|
26
|
+
scope = Scope.empty(environment: project_environment(configuration))
|
|
27
|
+
scanner = Inference::PrecisionScanner.new(scope: scope)
|
|
28
|
+
accumulator = CoverageAccumulator.new
|
|
29
|
+
files.each { |path| scan_into(path, scanner, accumulator, configuration) }
|
|
30
|
+
accumulator.to_report(files, {})
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def project_environment(configuration)
|
|
34
|
+
Environment.for_project(
|
|
35
|
+
libraries: configuration.libraries,
|
|
36
|
+
signature_paths: configuration.signature_paths
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Parses one file and feeds the scan result (or a parse-error
|
|
41
|
+
# record) into `accumulator`. `scanner` / `accumulator` are a
|
|
42
|
+
# matched pair — a `PrecisionScanner` + `CoverageAccumulator`, or a
|
|
43
|
+
# `ProtectionScanner` + `ProtectionAccumulator` — both of which
|
|
44
|
+
# respond to `scan(node)` and `absorb(path, result)`.
|
|
45
|
+
def scan_into(path, scanner, accumulator, configuration)
|
|
46
|
+
source = File.read(path)
|
|
47
|
+
parse_result = Prism.parse(source, filepath: path, version: configuration.target_ruby)
|
|
48
|
+
if parse_result.errors.any?
|
|
49
|
+
accumulator.record_parse_error(path, parse_result.errors)
|
|
50
|
+
return
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
accumulator.absorb(path, scanner.scan(parse_result.value))
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -95,6 +95,7 @@ module Rigor
|
|
|
95
95
|
render_section("Fires when:", entry.fires_when)
|
|
96
96
|
render_section("Does not fire when:", entry.does_not_fire_when)
|
|
97
97
|
@out.puts("Suppression: #{entry.suppression}")
|
|
98
|
+
@out.puts("Documentation: #{entry.documentation_url}")
|
|
98
99
|
@out.puts("Since: rigor #{entry.since}")
|
|
99
100
|
end
|
|
100
101
|
|
|
@@ -109,6 +110,7 @@ module Rigor
|
|
|
109
110
|
@out.puts("Authored severity: :#{entry.severity_authored}")
|
|
110
111
|
profile_table = entry.severity_by_profile.map { |profile, sev| "#{profile} → :#{sev}" }.join(", ")
|
|
111
112
|
@out.puts("Severity by profile: #{profile_table}")
|
|
113
|
+
@out.puts("Evidence tier: #{entry.evidence_tier || 'n/a (informational)'}")
|
|
112
114
|
@out.puts("")
|
|
113
115
|
end
|
|
114
116
|
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
class CLI
|
|
7
|
+
# Renders a {FusedProtectionReport} (ADR-70) as text or JSON. The text form
|
|
8
|
+
# leads with the fused protected ratio (caught by *either* a type or a test),
|
|
9
|
+
# splits it into the two axes, then lists the unprotected breakages ("add a
|
|
10
|
+
# type or a test here") and the least-protected files. The framing is always
|
|
11
|
+
# *where to add protection*, never "your code is broken".
|
|
12
|
+
class FusedProtectionRenderer
|
|
13
|
+
TOP_CALLS = 15
|
|
14
|
+
TOP_FILES = 10
|
|
15
|
+
|
|
16
|
+
def initialize(out:)
|
|
17
|
+
@out = out
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def render(report, format:)
|
|
21
|
+
format == "json" ? render_json(report) : render_text(report)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def render_json(report)
|
|
27
|
+
@out.puts(JSON.pretty_generate(report.to_h))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def render_text(report)
|
|
31
|
+
pct = (report.ratio * 100).round(1)
|
|
32
|
+
@out.puts "Fused protection (static type ∪ dynamic test)"
|
|
33
|
+
@out.puts " protected: #{report.protected_total} / #{report.grand_total} (#{pct}%)"
|
|
34
|
+
@out.puts " by type: #{report.total_type_killed}"
|
|
35
|
+
@out.puts " by test: #{report.total_test_killed} (type-survivors a test caught)"
|
|
36
|
+
@out.puts " unprotected: #{report.total_unprotected} (neither — add a type or a test)"
|
|
37
|
+
render_unprotected(report)
|
|
38
|
+
render_files(report)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def render_unprotected(report)
|
|
42
|
+
unprotected = report.unprotected
|
|
43
|
+
return if unprotected.empty?
|
|
44
|
+
|
|
45
|
+
@out.puts "\nAdd protection here — breakages neither a type nor a test caught:"
|
|
46
|
+
unprotected.first(TOP_CALLS).each do |call|
|
|
47
|
+
@out.puts format(" %<count>-5d #%<method>s e.g. %<sites>s",
|
|
48
|
+
count: call.count, method: call.method_name, sites: call.examples.join(" "))
|
|
49
|
+
end
|
|
50
|
+
@out.puts " (#{unprotected.size - TOP_CALLS} more)" if unprotected.size > TOP_CALLS
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def render_files(report)
|
|
54
|
+
worst = report.files.reject { |f| f.unprotected.zero? }.sort_by(&:ratio).first(TOP_FILES)
|
|
55
|
+
return if worst.empty?
|
|
56
|
+
|
|
57
|
+
@out.puts "\nLeast-protected files:"
|
|
58
|
+
worst.each do |file|
|
|
59
|
+
total = file.type_killed + file.test_killed + file.unprotected
|
|
60
|
+
protected_n = file.type_killed + file.test_killed
|
|
61
|
+
@out.puts format(" %<pct>5.1f%% %<path>s (%<n>d/%<total>d protected)",
|
|
62
|
+
pct: file.ratio * 100, path: file.path, n: protected_n, total: total)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
class CLI
|
|
5
|
+
# ADR-70 — aggregates per-file {Protection::MutationScanner::FusedFileResult}
|
|
6
|
+
# into a project-level **fused** protection report: how many type-visible
|
|
7
|
+
# breakages were caught by the type checker, how many of the *type-survivors*
|
|
8
|
+
# were caught by the test suite, and which sites neither axis caught — the
|
|
9
|
+
# ranked "add a type OR a test here" list.
|
|
10
|
+
#
|
|
11
|
+
# Framing (ADR-63 / ADR-62 Criterion A, extended): the payload is the
|
|
12
|
+
# **attribution** — which protection axis is missing — never raw survival.
|
|
13
|
+
# An unprotected site is "add protection here", never "your code is broken".
|
|
14
|
+
FusedFileProtection = Data.define(:path, :type_killed, :test_killed, :unprotected, :ratio)
|
|
15
|
+
UnprotectedBreakage = Data.define(:method_name, :count, :examples)
|
|
16
|
+
|
|
17
|
+
FusedProtectionReport = Data.define(:files, :unprotected, :parse_errors) do
|
|
18
|
+
def total_type_killed = files.sum(&:type_killed)
|
|
19
|
+
def total_test_killed = files.sum(&:test_killed)
|
|
20
|
+
def total_unprotected = files.sum(&:unprotected)
|
|
21
|
+
def grand_total = total_type_killed + total_test_killed + total_unprotected
|
|
22
|
+
def protected_total = total_type_killed + total_test_killed
|
|
23
|
+
def ratio = grand_total.zero? ? 1.0 : protected_total.to_f / grand_total
|
|
24
|
+
|
|
25
|
+
def to_h
|
|
26
|
+
{
|
|
27
|
+
"mode" => "protection-fused",
|
|
28
|
+
"type_killed" => total_type_killed,
|
|
29
|
+
"test_killed" => total_test_killed,
|
|
30
|
+
"unprotected" => total_unprotected,
|
|
31
|
+
"protected_ratio" => ratio.round(4),
|
|
32
|
+
"files" => files.map do |f|
|
|
33
|
+
{ "path" => f.path, "type_killed" => f.type_killed, "test_killed" => f.test_killed,
|
|
34
|
+
"unprotected" => f.unprotected, "ratio" => f.ratio.round(4) }
|
|
35
|
+
end,
|
|
36
|
+
"add_protection_here" => unprotected.map do |m|
|
|
37
|
+
{ "method" => m.method_name, "count" => m.count, "examples" => m.examples }
|
|
38
|
+
end,
|
|
39
|
+
"parse_errors" => parse_errors
|
|
40
|
+
}
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
class FusedProtectionAccumulator
|
|
45
|
+
def initialize
|
|
46
|
+
@files = []
|
|
47
|
+
@unprotected = Hash.new { |h, k| h[k] = { count: 0, examples: [] } }
|
|
48
|
+
@parse_errors = []
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def absorb(file_result)
|
|
52
|
+
@files << FusedFileProtection.new(
|
|
53
|
+
path: file_result.path, type_killed: file_result.type_killed,
|
|
54
|
+
test_killed: file_result.test_killed, unprotected: file_result.unprotected,
|
|
55
|
+
ratio: file_result.ratio
|
|
56
|
+
)
|
|
57
|
+
file_result.sites.each do |site|
|
|
58
|
+
bucket = @unprotected[site.method_name]
|
|
59
|
+
bucket[:count] += 1
|
|
60
|
+
bucket[:examples] << "#{file_result.path}:#{site.line}" if bucket[:examples].size < 3
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def record_parse_error(path, errors)
|
|
65
|
+
@parse_errors << { "path" => path, "errors" => errors.size }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def to_report
|
|
69
|
+
unprotected = @unprotected
|
|
70
|
+
.map { |m, v| UnprotectedBreakage.new(method_name: m, count: v[:count], examples: v[:examples]) }
|
|
71
|
+
.sort_by { |m| [-m.count, m.method_name] }
|
|
72
|
+
FusedProtectionReport.new(files: @files, unprotected: unprotected, parse_errors: @parse_errors)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "command"
|
|
4
|
+
require_relative "options"
|
|
4
5
|
|
|
5
6
|
require "optionparser"
|
|
6
7
|
|
|
@@ -8,11 +9,8 @@ module Rigor
|
|
|
8
9
|
class CLI
|
|
9
10
|
# Executes the `rigor lsp` command.
|
|
10
11
|
#
|
|
12
|
+
# Starts a long-running LSP server over stdio (JSON-RPC).
|
|
11
13
|
# See `docs/design/20260517-language-server.md` for the design.
|
|
12
|
-
# Slice 1 (this commit) ships the CLI subcommand entry point.
|
|
13
|
-
# The actual stdio JSON-RPC reader / writer is queued for slice 2;
|
|
14
|
-
# invoking `rigor lsp` at slice 1 returns immediately after
|
|
15
|
-
# validating the transport flag.
|
|
16
14
|
class LspCommand < Command
|
|
17
15
|
USAGE = "Usage: rigor lsp [options]"
|
|
18
16
|
|
|
@@ -109,9 +107,7 @@ module Rigor
|
|
|
109
107
|
opts.on("--log=PATH", "Write LSP wire log + server debug to PATH (default: stderr)") do |value|
|
|
110
108
|
options[:log] = value
|
|
111
109
|
end
|
|
112
|
-
|
|
113
|
-
options[:config] = value
|
|
114
|
-
end
|
|
110
|
+
Options.add_config(opts, options)
|
|
115
111
|
end
|
|
116
112
|
parser.parse!(@argv)
|
|
117
113
|
options
|