rigortype 0.1.19 → 0.2.0
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/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 +115 -16
- data/lib/rigor/cli/coverage_command.rb +148 -16
- data/lib/rigor/cli/coverage_scan.rb +57 -0
- data/lib/rigor/cli/explain_command.rb +2 -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/configuration/dependencies.rb +2 -4
- data/lib/rigor/configuration.rb +45 -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 +49 -5
- data/lib/rigor/environment.rb +17 -7
- 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 +156 -21
- 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 +244 -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/mutation_scanner.rb +120 -0
- data/lib/rigor/protection/mutator.rb +246 -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/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 +19 -1
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "English"
|
|
3
4
|
require "optionparser"
|
|
4
5
|
require "prism"
|
|
5
6
|
|
|
6
7
|
require_relative "../configuration"
|
|
8
|
+
require_relative "options"
|
|
7
9
|
require_relative "../environment"
|
|
8
10
|
require_relative "../inference/precision_scanner"
|
|
11
|
+
require_relative "../inference/protection_scanner"
|
|
12
|
+
require_relative "../inference/parameter_inference_collector"
|
|
13
|
+
require_relative "../protection/mutation_scanner"
|
|
14
|
+
require_relative "../language_server/project_context"
|
|
9
15
|
require_relative "../scope"
|
|
10
16
|
require_relative "coverage_report"
|
|
11
17
|
require_relative "coverage_renderer"
|
|
18
|
+
require_relative "coverage_scan"
|
|
19
|
+
require_relative "protection_report"
|
|
20
|
+
require_relative "protection_renderer"
|
|
21
|
+
require_relative "mutation_protection_report"
|
|
22
|
+
require_relative "mutation_protection_renderer"
|
|
12
23
|
require_relative "command"
|
|
13
24
|
|
|
14
25
|
module Rigor
|
|
@@ -32,10 +43,15 @@ module Rigor
|
|
|
32
43
|
# @return [Integer] CLI exit status.
|
|
33
44
|
def run
|
|
34
45
|
options = parse_options
|
|
46
|
+
return mutation_misuse_error if options[:mutation] && !options[:protection]
|
|
47
|
+
return run_mutation_protection(options) if options[:mutation]
|
|
48
|
+
|
|
35
49
|
paths = collect_paths(@argv, command_name: "coverage")
|
|
36
50
|
return CLI::EXIT_USAGE if paths.nil?
|
|
37
51
|
return usage_error if paths.empty?
|
|
38
52
|
|
|
53
|
+
return run_protection(paths, options) if options[:protection]
|
|
54
|
+
|
|
39
55
|
report = scan_paths(paths, options)
|
|
40
56
|
CoverageRenderer.new(out: @out).render(report, format: options.fetch(:format))
|
|
41
57
|
determine_exit(report, options)
|
|
@@ -44,45 +60,112 @@ module Rigor
|
|
|
44
60
|
private
|
|
45
61
|
|
|
46
62
|
def parse_options
|
|
47
|
-
options = { format: "text", threshold: nil, config: nil }
|
|
63
|
+
options = { format: "text", threshold: nil, config: nil, protection: false, mutation: false }
|
|
48
64
|
|
|
49
65
|
OptionParser.new do |opts|
|
|
50
66
|
opts.banner = USAGE
|
|
51
67
|
opts.on("--format=FORMAT", "Output format: text or json") { |v| options[:format] = v }
|
|
52
|
-
|
|
68
|
+
Options.add_config(opts, options)
|
|
69
|
+
opts.on(
|
|
70
|
+
"--protection",
|
|
71
|
+
"Report type-protection coverage (ADR-63 Tier 1) instead of type precision"
|
|
72
|
+
) { options[:protection] = true }
|
|
73
|
+
opts.on(
|
|
74
|
+
"--mutation",
|
|
75
|
+
"With --protection: measure actual mutation effectiveness (ADR-63 Tier 2). " \
|
|
76
|
+
"Scopes to git-changed files when no paths are given; explicit paths override."
|
|
77
|
+
) { options[:mutation] = true }
|
|
53
78
|
opts.on(
|
|
54
79
|
"--threshold=RATIO", Float,
|
|
55
|
-
"Exit 1 when precision ratio is below RATIO (0.0–1.0)"
|
|
80
|
+
"Exit 1 when the precision (or, with --protection, protection/effectiveness) ratio is below RATIO (0.0–1.0)"
|
|
56
81
|
) { |v| options[:threshold] = v }
|
|
57
82
|
end.parse!(@argv)
|
|
58
83
|
|
|
59
84
|
options
|
|
60
85
|
end
|
|
61
86
|
|
|
62
|
-
def
|
|
63
|
-
@err.puts("coverage:
|
|
87
|
+
def mutation_misuse_error
|
|
88
|
+
@err.puts("coverage: --mutation requires --protection")
|
|
64
89
|
@err.puts(USAGE)
|
|
65
90
|
CLI::EXIT_USAGE
|
|
66
91
|
end
|
|
67
92
|
|
|
68
|
-
def
|
|
93
|
+
def run_protection(paths, options)
|
|
94
|
+
report = scan_protection(paths, options)
|
|
95
|
+
ProtectionRenderer.new(out: @out).render(report, format: options.fetch(:format))
|
|
96
|
+
determine_protection_exit(report, options)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def scan_protection(paths, options)
|
|
69
100
|
configuration = Configuration.load(options.fetch(:config))
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
101
|
+
environment = project_environment(configuration)
|
|
102
|
+
scope = scope_with_inferred_params(paths, configuration, environment)
|
|
103
|
+
scanner = Inference::ProtectionScanner.new(scope: scope)
|
|
104
|
+
accumulator = ProtectionAccumulator.new
|
|
73
105
|
|
|
74
106
|
paths.each { |path| scan_one(path, scanner, accumulator, configuration) }
|
|
75
|
-
accumulator.to_report
|
|
107
|
+
accumulator.to_report
|
|
76
108
|
end
|
|
77
109
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
110
|
+
# ADR-67 WD3 — seed the call-site parameter-inference table so the
|
|
111
|
+
# protection scan counts an inferred-parameter receiver (e.g. `node.loc`
|
|
112
|
+
# where `node` is a `def compile(node)` parameter) as protected when its
|
|
113
|
+
# call sites resolve to concrete argument types. ONLY the parameter table
|
|
114
|
+
# is seeded — no cross-file discovery — so every site that does not gain
|
|
115
|
+
# an inferred parameter type is classified byte-identically to the
|
|
116
|
+
# un-inferred baseline. Collection spans the scanned `paths`.
|
|
117
|
+
def scope_with_inferred_params(paths, configuration, environment)
|
|
118
|
+
base = Scope.empty(environment: environment)
|
|
119
|
+
table = Inference::ParameterInferenceCollector.collect(
|
|
120
|
+
files: paths, environment: environment, target_ruby: configuration.target_ruby
|
|
82
121
|
)
|
|
122
|
+
return base if table.empty?
|
|
123
|
+
|
|
124
|
+
base.with_discovery(base.discovery.with(param_inferred_types: table))
|
|
83
125
|
end
|
|
84
126
|
|
|
85
|
-
def
|
|
127
|
+
def determine_protection_exit(report, options)
|
|
128
|
+
return 1 unless report.parse_errors.empty?
|
|
129
|
+
|
|
130
|
+
threshold = options[:threshold]
|
|
131
|
+
return 0 if threshold.nil?
|
|
132
|
+
|
|
133
|
+
report.ratio < threshold ? 1 : 0
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# ADR-63 Tier 2 — the mutation-effectiveness deep dive. Builds the RBS
|
|
137
|
+
# environment + project pre-pass once (the warm loop), then re-analyses
|
|
138
|
+
# each target file's mutants against its clean baseline. Defaults to the
|
|
139
|
+
# git-changed `.rb` files; explicit paths override (and enable the
|
|
140
|
+
# whole-project opt-in, which is minutes).
|
|
141
|
+
def run_mutation_protection(options)
|
|
142
|
+
explicit = collect_paths(@argv, command_name: "coverage")
|
|
143
|
+
return CLI::EXIT_USAGE if explicit.nil?
|
|
144
|
+
|
|
145
|
+
target_files = explicit.empty? ? changed_ruby_files : explicit
|
|
146
|
+
if target_files.empty?
|
|
147
|
+
@out.puts("No changed Ruby files to measure — pass paths to measure explicitly.")
|
|
148
|
+
return 0
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
report = scan_mutation_protection(target_files, options)
|
|
152
|
+
MutationProtectionRenderer.new(out: @out).render(report, format: options.fetch(:format))
|
|
153
|
+
determine_protection_exit(report, options)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def scan_mutation_protection(paths, options)
|
|
157
|
+
configuration = Configuration.load(options.fetch(:config))
|
|
158
|
+
context = LanguageServer::ProjectContext.new(configuration: configuration)
|
|
159
|
+
scanner = Protection::MutationScanner.new(
|
|
160
|
+
configuration: configuration, environment: context.environment, project_scan: context.project_scan
|
|
161
|
+
)
|
|
162
|
+
accumulator = MutationProtectionAccumulator.new
|
|
163
|
+
|
|
164
|
+
paths.each { |path| scan_mutation_one(path, scanner, accumulator, configuration) }
|
|
165
|
+
accumulator.to_report
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def scan_mutation_one(path, scanner, accumulator, configuration)
|
|
86
169
|
source = File.read(path)
|
|
87
170
|
parse_result = Prism.parse(source, filepath: path, version: configuration.target_ruby)
|
|
88
171
|
if parse_result.errors.any?
|
|
@@ -90,7 +173,56 @@ module Rigor
|
|
|
90
173
|
return
|
|
91
174
|
end
|
|
92
175
|
|
|
93
|
-
accumulator.absorb(path,
|
|
176
|
+
accumulator.absorb(scanner.scan_file(path, source: source))
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# The git-changed (modified / added / untracked) `.rb` files that exist on
|
|
180
|
+
# disk — the default Tier 2 scope. Returns [] outside a git work tree or
|
|
181
|
+
# when git is unavailable; the caller then reports "nothing to measure".
|
|
182
|
+
def changed_ruby_files
|
|
183
|
+
output = git_status_porcelain
|
|
184
|
+
return [] if output.nil?
|
|
185
|
+
|
|
186
|
+
output.each_line.filter_map { |line| changed_path(line) }.uniq.select { |p| File.file?(p) }
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Parse one `git status --porcelain` line (`XY <path>`, or `R old -> new`)
|
|
190
|
+
# into a candidate `.rb` path, or nil.
|
|
191
|
+
def changed_path(line)
|
|
192
|
+
path = line[3..]&.chomp
|
|
193
|
+
return nil if path.nil? || path.empty?
|
|
194
|
+
|
|
195
|
+
path = path.split(" -> ", 2).last if path.include?(" -> ")
|
|
196
|
+
path = path.delete_prefix('"').delete_suffix('"')
|
|
197
|
+
path.end_with?(".rb") ? path : nil
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def git_status_porcelain
|
|
201
|
+
output = IO.popen(%w[git status --porcelain --untracked-files=all], err: File::NULL, &:read)
|
|
202
|
+
$CHILD_STATUS&.success? ? output : nil
|
|
203
|
+
rescue SystemCallError
|
|
204
|
+
nil
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def usage_error
|
|
208
|
+
@err.puts("coverage: at least one path is required")
|
|
209
|
+
@err.puts(USAGE)
|
|
210
|
+
CLI::EXIT_USAGE
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def scan_paths(paths, options)
|
|
214
|
+
CoverageScan.precision_report(files: paths, configuration: Configuration.load(options.fetch(:config)))
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Delegated to the shared scan module (see {CoverageScan}); the
|
|
218
|
+
# protection path below reuses both, and `rigor check --coverage`
|
|
219
|
+
# reuses `precision_report` over the same machinery.
|
|
220
|
+
def project_environment(configuration)
|
|
221
|
+
CoverageScan.project_environment(configuration)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def scan_one(path, scanner, accumulator, configuration)
|
|
225
|
+
CoverageScan.scan_into(path, scanner, accumulator, configuration)
|
|
94
226
|
end
|
|
95
227
|
|
|
96
228
|
def determine_exit(report, options)
|
|
@@ -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
|
|
|
@@ -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
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
class CLI
|
|
7
|
+
# Renders a {MutationProtectionReport} (ADR-63 Tier 2) as text or JSON. The
|
|
8
|
+
# text form leads with the effectiveness ratio (caught breakages), then the
|
|
9
|
+
# breakages Rigor missed ("add a type here"), then the least-effective files.
|
|
10
|
+
# The framing is always *where to add a type*, never "your code is broken".
|
|
11
|
+
class MutationProtectionRenderer
|
|
12
|
+
TOP_CALLS = 15
|
|
13
|
+
TOP_FILES = 10
|
|
14
|
+
|
|
15
|
+
def initialize(out:)
|
|
16
|
+
@out = out
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def render(report, format:)
|
|
20
|
+
format == "json" ? render_json(report) : render_text(report)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def render_json(report)
|
|
26
|
+
@out.puts(JSON.pretty_generate(report.to_h))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def render_text(report)
|
|
30
|
+
pct = (report.ratio * 100).round(1)
|
|
31
|
+
@out.puts "Type-protection effectiveness (Tier 2 — mutation kill rate)"
|
|
32
|
+
@out.puts " caught breakages: #{report.total_killed} / #{report.grand_total} (#{pct}%)"
|
|
33
|
+
@out.puts " (effectiveness = when a type-visible bug was introduced, Rigor caught it)"
|
|
34
|
+
render_missed(report)
|
|
35
|
+
render_files(report)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def render_missed(report)
|
|
39
|
+
missed = report.missed
|
|
40
|
+
return if missed.empty?
|
|
41
|
+
|
|
42
|
+
@out.puts "\nAdd a type here — breakages Rigor missed (a wrong call that stayed silent):"
|
|
43
|
+
missed.first(TOP_CALLS).each do |call|
|
|
44
|
+
@out.puts format(" %<count>-5d #%<method>s e.g. %<sites>s",
|
|
45
|
+
count: call.count, method: call.method_name, sites: call.examples.join(" "))
|
|
46
|
+
end
|
|
47
|
+
@out.puts " (#{missed.size - TOP_CALLS} more)" if missed.size > TOP_CALLS
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def render_files(report)
|
|
51
|
+
worst = report.files.reject { |f| f.survived.zero? }.sort_by(&:ratio).first(TOP_FILES)
|
|
52
|
+
return if worst.empty?
|
|
53
|
+
|
|
54
|
+
@out.puts "\nLeast-effective files:"
|
|
55
|
+
worst.each do |file|
|
|
56
|
+
total = file.killed + file.survived
|
|
57
|
+
@out.puts format(" %<pct>5.1f%% %<path>s (%<n>d/%<total>d breakages caught)",
|
|
58
|
+
pct: file.ratio * 100, path: file.path, n: file.killed, total: total)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
class CLI
|
|
5
|
+
# ADR-63 Tier 2 — aggregates per-file {Protection::MutationScanner}
|
|
6
|
+
# results into a project-level *effectiveness* report: the kill ratio (when
|
|
7
|
+
# a type-visible bug was introduced, how often Rigor caught it), the per-file
|
|
8
|
+
# breakdown, and a ranked "add a type here" list keyed by the method whose
|
|
9
|
+
# breakage Rigor most often *missed* — the sites where a receiver annotation
|
|
10
|
+
# would buy real catching power.
|
|
11
|
+
#
|
|
12
|
+
# The framing is load-bearing (ADR-63 Criterion A / ADR-62 Criterion A): the
|
|
13
|
+
# number is *effectiveness*, the survivors are *missed breakages / where to
|
|
14
|
+
# add a type*, never "your code is broken".
|
|
15
|
+
FileEffectiveness = Data.define(:path, :killed, :survived, :ratio)
|
|
16
|
+
MissedBreakage = Data.define(:method_name, :count, :examples)
|
|
17
|
+
|
|
18
|
+
MutationProtectionReport = Data.define(:files, :missed, :parse_errors) do
|
|
19
|
+
def total_killed = files.sum(&:killed)
|
|
20
|
+
def total_survived = files.sum(&:survived)
|
|
21
|
+
def grand_total = total_killed + total_survived
|
|
22
|
+
def ratio = grand_total.zero? ? 1.0 : total_killed.to_f / grand_total
|
|
23
|
+
|
|
24
|
+
def to_h
|
|
25
|
+
{
|
|
26
|
+
"mode" => "mutation",
|
|
27
|
+
"killed" => total_killed,
|
|
28
|
+
"survived" => total_survived,
|
|
29
|
+
"effectiveness_ratio" => ratio.round(4),
|
|
30
|
+
"files" => files.map do |f|
|
|
31
|
+
{ "path" => f.path, "killed" => f.killed,
|
|
32
|
+
"survived" => f.survived, "ratio" => f.ratio.round(4) }
|
|
33
|
+
end,
|
|
34
|
+
"add_a_type_here" => missed.map do |m|
|
|
35
|
+
{ "method" => m.method_name, "count" => m.count, "examples" => m.examples }
|
|
36
|
+
end,
|
|
37
|
+
"parse_errors" => parse_errors
|
|
38
|
+
}
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
class MutationProtectionAccumulator
|
|
43
|
+
def initialize
|
|
44
|
+
@files = []
|
|
45
|
+
@missed = Hash.new { |h, k| h[k] = { count: 0, examples: [] } }
|
|
46
|
+
@parse_errors = []
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def absorb(file_result)
|
|
50
|
+
@files << FileEffectiveness.new(
|
|
51
|
+
path: file_result.path, killed: file_result.killed,
|
|
52
|
+
survived: file_result.survived, ratio: file_result.ratio
|
|
53
|
+
)
|
|
54
|
+
file_result.sites.each do |site|
|
|
55
|
+
bucket = @missed[site.method_name]
|
|
56
|
+
bucket[:count] += 1
|
|
57
|
+
bucket[:examples] << "#{file_result.path}:#{site.line}" if bucket[:examples].size < 3
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def record_parse_error(path, errors)
|
|
62
|
+
@parse_errors << { "path" => path, "errors" => errors.size }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def to_report
|
|
66
|
+
missed = @missed
|
|
67
|
+
.map { |method, v| MissedBreakage.new(method_name: method, count: v[:count], examples: v[:examples]) }
|
|
68
|
+
.sort_by { |m| [-m.count, m.method_name] }
|
|
69
|
+
MutationProtectionReport.new(files: @files, missed: missed, parse_errors: @parse_errors)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
data/lib/rigor/cli/options.rb
CHANGED
|
@@ -17,6 +17,15 @@ module Rigor
|
|
|
17
17
|
module Options
|
|
18
18
|
module_function
|
|
19
19
|
|
|
20
|
+
# Defines the standard `--config=PATH` flag on `parser`, writing the
|
|
21
|
+
# path into `options[:config]`. Used by every subcommand that loads a
|
|
22
|
+
# `.rigor.yml`; the few whose `--config` help text is intentionally
|
|
23
|
+
# bespoke (`diff`, `mcp`, `show-bleedingedge`) keep their own
|
|
24
|
+
# `parser.on` rather than this shared wording.
|
|
25
|
+
def add_config(parser, options)
|
|
26
|
+
parser.on("--config=PATH", "Path to the Rigor configuration file") { |value| options[:config] = value }
|
|
27
|
+
end
|
|
28
|
+
|
|
20
29
|
# Defines the `--tmp-file` / `--instead-of` editor-mode flag pair
|
|
21
30
|
# on `parser`, writing into `options`.
|
|
22
31
|
def add_editor_mode(parser, options)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "optparse"
|
|
4
4
|
|
|
5
5
|
require_relative "../configuration"
|
|
6
|
+
require_relative "options"
|
|
6
7
|
require_relative "../plugin"
|
|
7
8
|
require_relative "../plugin/loader"
|
|
8
9
|
require_relative "../plugin/services"
|
|
@@ -102,7 +103,7 @@ module Rigor
|
|
|
102
103
|
options = { config: nil, format: "text", strict: false, capabilities: false }
|
|
103
104
|
OptionParser.new do |opts|
|
|
104
105
|
opts.banner = USAGE
|
|
105
|
-
|
|
106
|
+
Options.add_config(opts, options)
|
|
106
107
|
opts.on("--format=FORMAT", "Output format: text (default) or json") { |v| options[:format] = v }
|
|
107
108
|
opts.on("--strict", "Exit 1 if any plugin failed to load (CI gate)") { options[:strict] = true }
|
|
108
109
|
opts.on("--capabilities", "Emit the per-plugin extension-protocol catalogue (ADR-37)") do
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
class CLI
|
|
7
|
+
# Renders an {ProtectionReport} (ADR-63 Tier 1) as text or JSON. The text
|
|
8
|
+
# form leads with the protected ratio, then the highest-traffic untyped
|
|
9
|
+
# dispatches ("add a type here"), then the lowest-protected files. The
|
|
10
|
+
# framing is always *where to add a type*, never "your code is broken".
|
|
11
|
+
class ProtectionRenderer
|
|
12
|
+
TOP_CALLS = 15
|
|
13
|
+
TOP_FILES = 10
|
|
14
|
+
|
|
15
|
+
def initialize(out:)
|
|
16
|
+
@out = out
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def render(report, format:)
|
|
20
|
+
format == "json" ? render_json(report) : render_text(report)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def render_json(report)
|
|
26
|
+
@out.puts(JSON.pretty_generate(report.to_h))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def render_text(report)
|
|
30
|
+
pct = (report.ratio * 100).round(1)
|
|
31
|
+
@out.puts "Type-protection coverage (Tier 1 — dispatch-site receiver concreteness)"
|
|
32
|
+
@out.puts " protected dispatch sites: #{report.total_protected} / #{report.grand_total} (#{pct}%)"
|
|
33
|
+
@out.puts " (protected = Rigor can catch a wrong call here; an upper bound on real protection)"
|
|
34
|
+
render_untyped_calls(report)
|
|
35
|
+
render_files(report)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def render_untyped_calls(report)
|
|
39
|
+
calls = report.untyped_calls
|
|
40
|
+
return if calls.empty?
|
|
41
|
+
|
|
42
|
+
@out.puts "\nAdd a type here — methods most often called on an untyped receiver:"
|
|
43
|
+
calls.first(TOP_CALLS).each do |call|
|
|
44
|
+
@out.puts format(" %<count>-5d #%<method>s e.g. %<sites>s",
|
|
45
|
+
count: call.count, method: call.method_name, sites: call.examples.join(" "))
|
|
46
|
+
end
|
|
47
|
+
@out.puts " (#{calls.size - TOP_CALLS} more)" if calls.size > TOP_CALLS
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def render_files(report)
|
|
51
|
+
worst = report.files.reject { |f| f.unprotected_count.zero? }.sort_by(&:ratio).first(TOP_FILES)
|
|
52
|
+
return if worst.empty?
|
|
53
|
+
|
|
54
|
+
@out.puts "\nLeast-protected files:"
|
|
55
|
+
worst.each do |file|
|
|
56
|
+
total = file.protected_count + file.unprotected_count
|
|
57
|
+
@out.puts format(" %<pct>5.1f%% %<path>s (%<n>d/%<total>d protected)",
|
|
58
|
+
pct: file.ratio * 100, path: file.path, n: file.protected_count, total: total)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
class CLI
|
|
5
|
+
# ADR-63 Tier 1 — aggregates per-file {Inference::ProtectionScanner}
|
|
6
|
+
# results into a project-level protection report: the protected ratio, the
|
|
7
|
+
# per-file breakdown, and a ranked "add a type here" list keyed by the
|
|
8
|
+
# method called on an unprotected (`Dynamic`) receiver — the highest-traffic
|
|
9
|
+
# untyped dispatches, where a receiver annotation buys the most catching
|
|
10
|
+
# power.
|
|
11
|
+
FileProtection = Data.define(:path, :protected_count, :unprotected_count, :ratio)
|
|
12
|
+
UntypedCall = Data.define(:method_name, :count, :examples)
|
|
13
|
+
|
|
14
|
+
ProtectionReport = Data.define(:files, :untyped_calls, :parse_errors) do
|
|
15
|
+
def total_protected = files.sum(&:protected_count)
|
|
16
|
+
def total_unprotected = files.sum(&:unprotected_count)
|
|
17
|
+
def grand_total = total_protected + total_unprotected
|
|
18
|
+
def ratio = grand_total.zero? ? 1.0 : total_protected.to_f / grand_total
|
|
19
|
+
|
|
20
|
+
def to_h
|
|
21
|
+
{
|
|
22
|
+
"protected" => total_protected,
|
|
23
|
+
"unprotected" => total_unprotected,
|
|
24
|
+
"protection_ratio" => ratio.round(4),
|
|
25
|
+
"files" => files.map do |f|
|
|
26
|
+
{ "path" => f.path, "protected" => f.protected_count,
|
|
27
|
+
"unprotected" => f.unprotected_count, "ratio" => f.ratio.round(4) }
|
|
28
|
+
end,
|
|
29
|
+
"add_a_type_here" => untyped_calls.map do |c|
|
|
30
|
+
{ "method" => c.method_name, "count" => c.count, "examples" => c.examples }
|
|
31
|
+
end,
|
|
32
|
+
"parse_errors" => parse_errors
|
|
33
|
+
}
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class ProtectionAccumulator
|
|
38
|
+
def initialize
|
|
39
|
+
@files = []
|
|
40
|
+
@calls = Hash.new { |h, k| h[k] = { count: 0, examples: [] } }
|
|
41
|
+
@parse_errors = []
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def absorb(path, file_result)
|
|
45
|
+
@files << FileProtection.new(
|
|
46
|
+
path: path, protected_count: file_result.protected_count,
|
|
47
|
+
unprotected_count: file_result.unprotected_count, ratio: file_result.ratio
|
|
48
|
+
)
|
|
49
|
+
file_result.sites.each do |site|
|
|
50
|
+
bucket = @calls[site.method_name]
|
|
51
|
+
bucket[:count] += 1
|
|
52
|
+
bucket[:examples] << "#{path}:#{site.line}" if bucket[:examples].size < 3
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def record_parse_error(path, errors)
|
|
57
|
+
@parse_errors << { "path" => path, "errors" => errors.size }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def to_report
|
|
61
|
+
untyped = @calls
|
|
62
|
+
.map { |method, v| UntypedCall.new(method_name: method, count: v[:count], examples: v[:examples]) }
|
|
63
|
+
.sort_by { |c| [-c.count, c.method_name] }
|
|
64
|
+
ProtectionReport.new(files: @files, untyped_calls: untyped, parse_errors: @parse_errors)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "optionparser"
|
|
4
4
|
|
|
5
5
|
require_relative "../configuration"
|
|
6
|
+
require_relative "options"
|
|
6
7
|
require_relative "../sig_gen"
|
|
7
8
|
require_relative "command"
|
|
8
9
|
|
|
@@ -145,7 +146,7 @@ module Rigor
|
|
|
145
146
|
opts.on("--tighter-returns", "Emit only tighter-return classifications") do
|
|
146
147
|
options[:selection] << SigGen::Classification::TIGHTER_RETURN
|
|
147
148
|
end
|
|
148
|
-
|
|
149
|
+
Options.add_config(opts, options)
|
|
149
150
|
end
|
|
150
151
|
end
|
|
151
152
|
|
|
@@ -5,6 +5,7 @@ require "optionparser"
|
|
|
5
5
|
require "prism"
|
|
6
6
|
|
|
7
7
|
require_relative "../configuration"
|
|
8
|
+
require_relative "options"
|
|
8
9
|
require_relative "../environment"
|
|
9
10
|
require_relative "../scope"
|
|
10
11
|
require_relative "../inference/flow_tracer"
|
|
@@ -63,7 +64,7 @@ module Rigor
|
|
|
63
64
|
options[:line] = value
|
|
64
65
|
end
|
|
65
66
|
opts.on("--verbose", "Include every expression enter/result frame") { options[:verbose] = true }
|
|
66
|
-
|
|
67
|
+
Options.add_config(opts, options)
|
|
67
68
|
end
|
|
68
69
|
parser.parse!(@argv)
|
|
69
70
|
options
|