rigortype 0.1.16 → 0.1.18
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 +4 -2
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +18 -1
- data/lib/rigor/analysis/check_rules/rule_walk.rb +67 -0
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +100 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +226 -0
- data/lib/rigor/analysis/check_rules.rb +180 -73
- data/lib/rigor/analysis/dependency_recorder.rb +122 -0
- data/lib/rigor/analysis/diagnostic.rb +18 -0
- data/lib/rigor/analysis/incremental.rb +162 -0
- data/lib/rigor/analysis/incremental_session.rb +337 -0
- data/lib/rigor/analysis/rule_catalog.rb +48 -0
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +580 -0
- data/lib/rigor/analysis/runner/pool_coordinator.rb +569 -0
- data/lib/rigor/analysis/runner/project_pre_passes.rb +318 -0
- data/lib/rigor/analysis/runner/run_snapshots.rb +46 -0
- data/lib/rigor/analysis/runner.rb +477 -1110
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +121 -0
- data/lib/rigor/analysis/worker_session.rb +47 -8
- data/lib/rigor/builtins/static_return_refinements.rb +7 -1
- data/lib/rigor/cache/descriptor.rb +50 -49
- data/lib/rigor/cache/incremental_snapshot.rb +153 -0
- data/lib/rigor/cache/rbs_cache_producer.rb +34 -0
- data/lib/rigor/cache/rbs_class_ancestor_table.rb +2 -8
- data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -8
- data/lib/rigor/cache/rbs_constant_table.rb +2 -8
- data/lib/rigor/cache/rbs_environment.rb +2 -8
- data/lib/rigor/cache/rbs_known_class_names.rb +2 -8
- data/lib/rigor/cache/store.rb +145 -14
- data/lib/rigor/cli/annotate_command.rb +2 -7
- data/lib/rigor/cli/baseline_command.rb +2 -7
- data/lib/rigor/cli/check_command.rb +705 -0
- data/lib/rigor/cli/ci_detector.rb +94 -0
- data/lib/rigor/cli/command.rb +47 -0
- data/lib/rigor/cli/coverage_command.rb +3 -23
- data/lib/rigor/cli/coverage_renderer.rb +3 -8
- data/lib/rigor/cli/diagnostic_formats.rb +345 -0
- data/lib/rigor/cli/diff_command.rb +3 -7
- data/lib/rigor/cli/explain_command.rb +2 -7
- data/lib/rigor/cli/lsp_command.rb +3 -7
- data/lib/rigor/cli/mcp_command.rb +3 -7
- data/lib/rigor/cli/options.rb +57 -0
- data/lib/rigor/cli/plugin_command.rb +3 -7
- data/lib/rigor/cli/plugins_command.rb +2 -7
- data/lib/rigor/cli/prism_colorizer.rb +10 -3
- data/lib/rigor/cli/renderable.rb +26 -0
- data/lib/rigor/cli/sig_gen_command.rb +2 -7
- data/lib/rigor/cli/skill_command.rb +3 -7
- data/lib/rigor/cli/trace_command.rb +143 -0
- data/lib/rigor/cli/trace_renderer.rb +310 -0
- data/lib/rigor/cli/triage_command.rb +2 -7
- data/lib/rigor/cli/type_of_command.rb +5 -38
- data/lib/rigor/cli/type_of_renderer.rb +4 -9
- data/lib/rigor/cli/type_scan_command.rb +3 -23
- data/lib/rigor/cli/type_scan_renderer.rb +4 -9
- data/lib/rigor/cli.rb +15 -532
- data/lib/rigor/configuration/dependencies.rb +18 -1
- data/lib/rigor/configuration/severity_profile.rb +22 -3
- data/lib/rigor/configuration.rb +16 -3
- data/lib/rigor/environment/rbs_loader.rb +129 -71
- data/lib/rigor/environment.rb +1 -1
- data/lib/rigor/inference/acceptance.rb +10 -0
- data/lib/rigor/inference/block_parameter_binder.rb +1 -2
- data/lib/rigor/inference/builtins/array_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/comparable_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/complex_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/date_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/encoding_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/enumerable_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/exception_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/hash_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/method_catalog.rb +15 -0
- data/lib/rigor/inference/builtins/numeric_catalog.rb +21 -93
- data/lib/rigor/inference/builtins/pathname_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/proc_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/random_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/range_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/rational_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/re_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/set_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/string_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/struct_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/time_catalog.rb +2 -5
- data/lib/rigor/inference/expression_typer.rb +149 -63
- data/lib/rigor/inference/flow_tracer.rb +180 -0
- data/lib/rigor/inference/macro_block_self_type.rb +10 -11
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +5 -1
- data/lib/rigor/inference/method_dispatcher/call_context.rb +65 -0
- data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +11 -10
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +12 -6
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +246 -0
- data/lib/rigor/inference/method_dispatcher/file_folding.rb +6 -2
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -2
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +4 -1
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +4 -1
- data/lib/rigor/inference/method_dispatcher/math_folding.rb +6 -6
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +12 -7
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +33 -1
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +23 -13
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +9 -9
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +6 -6
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +120 -9
- data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +12 -12
- data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +49 -0
- data/lib/rigor/inference/method_dispatcher/time_folding.rb +6 -6
- data/lib/rigor/inference/method_dispatcher/uri_folding.rb +9 -9
- data/lib/rigor/inference/method_dispatcher.rb +185 -84
- data/lib/rigor/inference/narrowing.rb +262 -5
- data/lib/rigor/inference/scope_indexer.rb +208 -21
- data/lib/rigor/inference/statement_evaluator.rb +110 -48
- data/lib/rigor/language_server/buffer_resolution.rb +33 -0
- data/lib/rigor/language_server/completion_provider.rb +4 -4
- data/lib/rigor/language_server/document_symbol_provider.rb +4 -4
- data/lib/rigor/language_server/folding_range_provider.rb +4 -4
- data/lib/rigor/language_server/hover_provider.rb +4 -4
- data/lib/rigor/language_server/selection_range_provider.rb +4 -4
- data/lib/rigor/language_server/signature_help_provider.rb +4 -4
- data/lib/rigor/plugin/additional_initializer.rb +61 -38
- data/lib/rigor/plugin/base.rb +302 -45
- data/lib/rigor/plugin/node_rule_walk.rb +147 -0
- data/lib/rigor/plugin/registry.rb +281 -15
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/rbs_extended/conformance_checker.rb +293 -0
- data/lib/rigor/rbs_extended.rb +39 -0
- data/lib/rigor/scope/discovery_index.rb +58 -0
- data/lib/rigor/scope.rb +150 -167
- data/lib/rigor/sig_gen/observation_collector.rb +6 -6
- data/lib/rigor/source/literals.rb +14 -0
- data/lib/rigor/type/acceptance_router.rb +19 -0
- data/lib/rigor/type/accepts_result.rb +3 -10
- data/lib/rigor/type/app.rb +3 -7
- data/lib/rigor/type/bot.rb +2 -3
- data/lib/rigor/type/bound_method.rb +5 -12
- data/lib/rigor/type/combinator.rb +22 -0
- data/lib/rigor/type/constant.rb +2 -3
- data/lib/rigor/type/data_class.rb +80 -0
- data/lib/rigor/type/data_instance.rb +100 -0
- data/lib/rigor/type/difference.rb +5 -10
- data/lib/rigor/type/dynamic.rb +5 -10
- data/lib/rigor/type/hash_shape.rb +5 -15
- data/lib/rigor/type/integer_range.rb +5 -10
- data/lib/rigor/type/intersection.rb +5 -10
- data/lib/rigor/type/nominal.rb +5 -10
- data/lib/rigor/type/refined.rb +5 -10
- data/lib/rigor/type/singleton.rb +5 -10
- data/lib/rigor/type/top.rb +2 -3
- data/lib/rigor/type/tuple.rb +5 -10
- data/lib/rigor/type/union.rb +5 -10
- data/lib/rigor/type.rb +2 -0
- data/lib/rigor/value_semantics.rb +77 -0
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +2 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +70 -32
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -3
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +15 -21
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +1 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -2
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -2
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +12 -2
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +12 -2
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +35 -18
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +8 -29
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +17 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +2 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +83 -36
- data/sig/rigor/cache.rbs +19 -0
- data/sig/rigor/environment.rbs +0 -2
- data/sig/rigor/inference.rbs +27 -0
- data/sig/rigor/plugin/base.rbs +1 -2
- data/sig/rigor/rbs_extended.rbs +2 -0
- data/sig/rigor/scope.rbs +42 -25
- data/sig/rigor/source.rbs +1 -0
- data/sig/rigor/type.rbs +58 -1
- data/sig/rigor.rbs +6 -1
- data/skills/rigor-ci-setup/SKILL.md +319 -0
- metadata +36 -2
- data/lib/rigor/cache/rbs_instance_definitions.rb +0 -79
data/lib/rigor/cli.rb
CHANGED
|
@@ -9,13 +9,15 @@ require_relative "configuration"
|
|
|
9
9
|
require_relative "version"
|
|
10
10
|
require_relative "analysis/diagnostic"
|
|
11
11
|
require_relative "analysis/result"
|
|
12
|
+
require_relative "cli/options"
|
|
13
|
+
require_relative "cli/diagnostic_formats"
|
|
14
|
+
require_relative "cli/ci_detector"
|
|
12
15
|
|
|
13
16
|
module Rigor
|
|
14
17
|
# The CLI class is a dispatcher: each `run_*` method delegates to a
|
|
15
18
|
# command-specific class once the command grows beyond a few lines (see
|
|
16
|
-
# {CLI::TypeOfCommand}).
|
|
17
|
-
|
|
18
|
-
class CLI # rubocop:disable Metrics/ClassLength
|
|
19
|
+
# {CLI::TypeOfCommand} and {CLI::CheckCommand}).
|
|
20
|
+
class CLI
|
|
19
21
|
EXIT_USAGE = 64
|
|
20
22
|
|
|
21
23
|
HANDLERS = {
|
|
@@ -23,6 +25,7 @@ module Rigor
|
|
|
23
25
|
"init" => :run_init,
|
|
24
26
|
"annotate" => :run_annotate,
|
|
25
27
|
"type-of" => :run_type_of,
|
|
28
|
+
"trace" => :run_trace,
|
|
26
29
|
"type-scan" => :run_type_scan,
|
|
27
30
|
"explain" => :run_explain,
|
|
28
31
|
"diff" => :run_diff,
|
|
@@ -78,508 +81,9 @@ module Rigor
|
|
|
78
81
|
end
|
|
79
82
|
|
|
80
83
|
def run_check
|
|
81
|
-
|
|
82
|
-
options = parse_check_options
|
|
83
|
-
buffer = resolve_buffer_binding(options)
|
|
84
|
-
return EXIT_USAGE if buffer == :usage_error
|
|
85
|
-
|
|
86
|
-
configuration = load_check_configuration(options)
|
|
87
|
-
cache_root = configuration.cache_path
|
|
88
|
-
handle_clear_cache(cache_root) if options.fetch(:clear_cache)
|
|
89
|
-
|
|
90
|
-
runner = build_check_runner(
|
|
91
|
-
configuration: configuration, options: options,
|
|
92
|
-
buffer: buffer, cache_root: cache_root
|
|
93
|
-
)
|
|
94
|
-
raw_result = runner.run(@argv.empty? ? configuration.paths : @argv)
|
|
95
|
-
result = apply_baseline_filter(raw_result, configuration, options)
|
|
96
|
-
|
|
97
|
-
write_result(result, options.fetch(:format))
|
|
98
|
-
write_run_stats(result.stats) if result.stats
|
|
99
|
-
write_trace_appendices
|
|
100
|
-
write_cache_stats(cache_root, runner.cache_store) if options.fetch(:cache_stats)
|
|
101
|
-
|
|
102
|
-
exit_code = result.success? ? 0 : 1
|
|
103
|
-
exit_code = 1 if baseline_strict_violation?(raw_result.diagnostics, configuration, options)
|
|
104
|
-
exit_code
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
# ADR-22 slice 5 — the `--baseline-strict` CI gate. When the
|
|
108
|
-
# flag is set, ANY baseline drift fails the run — not only
|
|
109
|
-
# excess drift (a bucket over threshold, which already fails
|
|
110
|
-
# via the surfaced diagnostics) but also DEFICIT drift
|
|
111
|
-
# (`actual < count`: the baseline has grown looser than the
|
|
112
|
-
# code and should be regenerated). A no-op, with a stderr
|
|
113
|
-
# note, when no baseline is active — the flag never
|
|
114
|
-
# implicitly loads a baseline the config did not name (WD2).
|
|
115
|
-
def baseline_strict_violation?(raw_diagnostics, configuration, options)
|
|
116
|
-
return false unless options.fetch(:baseline_strict)
|
|
117
|
-
|
|
118
|
-
path = resolve_baseline_path(configuration, options)
|
|
119
|
-
if path.nil?
|
|
120
|
-
@err.puts("rigor: --baseline-strict given but no baseline is active; nothing to gate.")
|
|
121
|
-
return false
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
baseline = Analysis::Baseline.load(path, project_root: Dir.pwd)
|
|
125
|
-
return false if baseline.nil? || baseline.empty?
|
|
126
|
-
|
|
127
|
-
drifted = baseline.audit(raw_diagnostics).reject { |row| row.status == :within }
|
|
128
|
-
return false if drifted.empty?
|
|
129
|
-
|
|
130
|
-
report_strict_drift(drifted, path)
|
|
131
|
-
true
|
|
132
|
-
rescue Analysis::Baseline::LoadError => e
|
|
133
|
-
@err.puts("rigor: baseline load failed: #{e.message} (--baseline-strict gate skipped)")
|
|
134
|
-
false
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
def report_strict_drift(rows, path)
|
|
138
|
-
@err.puts("rigor: --baseline-strict — #{rows.size} bucket(s) drifted from #{path}:")
|
|
139
|
-
rows.sort_by { |r| [r.bucket.file, r.bucket.rule] }.each do |row|
|
|
140
|
-
delta = row.delta.positive? ? "+#{row.delta}" : row.delta.to_s
|
|
141
|
-
@err.puts(" #{row.bucket.file} [#{row.bucket.rule}] " \
|
|
142
|
-
"#{row.bucket.count} → #{row.actual_count} (Δ#{delta}, #{row.status})")
|
|
143
|
-
end
|
|
144
|
-
@err.puts("rigor: run `rigor baseline regenerate` to refresh the baseline.")
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
# ADR-22 — apply the baseline filter as the LAST step of
|
|
148
|
-
# the diagnostic pipeline (after `# rigor:disable`,
|
|
149
|
-
# `severity_profile`, etc. — WD6). Resolution order
|
|
150
|
-
# follows WD2 (b):
|
|
151
|
-
#
|
|
152
|
-
# 1. --no-baseline on the CLI → no baseline.
|
|
153
|
-
# 2. --baseline=PATH on the CLI → load that path.
|
|
154
|
-
# 3. .rigor.yml's `baseline: <path>` → load that path.
|
|
155
|
-
# 4. otherwise → no baseline.
|
|
156
|
-
#
|
|
157
|
-
# When the path resolves and loads successfully, the filter
|
|
158
|
-
# replaces `result.diagnostics` with the surfaced set and
|
|
159
|
-
# writes a one-line summary to stderr (WD7) when any
|
|
160
|
-
# diagnostics were silenced. Load failures emit a warning
|
|
161
|
-
# to stderr and fall through to "no baseline" (graceful
|
|
162
|
-
# degradation).
|
|
163
|
-
def apply_baseline_filter(result, configuration, options)
|
|
164
|
-
path = resolve_baseline_path(configuration, options)
|
|
165
|
-
return result if path.nil?
|
|
166
|
-
|
|
167
|
-
baseline = Analysis::Baseline.load(path, project_root: Dir.pwd)
|
|
168
|
-
return result if baseline.nil?
|
|
169
|
-
|
|
170
|
-
surfaced, silenced_count = baseline.filter(result.diagnostics)
|
|
171
|
-
report_baseline_summary(silenced_count, path) if silenced_count.positive?
|
|
172
|
-
Analysis::Result.new(diagnostics: surfaced, stats: result.stats)
|
|
173
|
-
rescue Analysis::Baseline::LoadError => e
|
|
174
|
-
@err.puts("rigor: baseline load failed: #{e.message} (continuing without baseline)")
|
|
175
|
-
result
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
# WD2 (b) — resolve effective baseline path.
|
|
179
|
-
def resolve_baseline_path(configuration, options)
|
|
180
|
-
cli_value = options.fetch(:baseline)
|
|
181
|
-
case cli_value
|
|
182
|
-
when false then nil # --no-baseline
|
|
183
|
-
when :unset then configuration.baseline_path # fall through to config
|
|
184
|
-
else cli_value # --baseline=PATH
|
|
185
|
-
end
|
|
186
|
-
end
|
|
187
|
-
|
|
188
|
-
def report_baseline_summary(silenced_count, baseline_path)
|
|
189
|
-
@err.puts("rigor: #{silenced_count} diagnostic(s) silenced by baseline #{baseline_path}")
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
def build_check_runner(configuration:, options:, buffer:, cache_root:)
|
|
193
|
-
cache_store = options.fetch(:no_cache) ? nil : Cache::Store.new(root: cache_root)
|
|
194
|
-
Analysis::Runner.new(
|
|
195
|
-
configuration: configuration,
|
|
196
|
-
explain: options.fetch(:explain),
|
|
197
|
-
cache_store: cache_store,
|
|
198
|
-
collect_stats: options.fetch(:stats),
|
|
199
|
-
workers: resolve_workers(options, configuration),
|
|
200
|
-
buffer: buffer
|
|
201
|
-
)
|
|
202
|
-
end
|
|
203
|
-
|
|
204
|
-
# Editor-mode CLI envelope. The `--tmp-file=PATH` /
|
|
205
|
-
# `--instead-of=PATH` pair binds an in-flight buffer file to
|
|
206
|
-
# the logical project path it represents (see
|
|
207
|
-
# `docs/design/20260516-editor-mode.md`). Both flags must
|
|
208
|
-
# appear together; either alone is a usage error. The
|
|
209
|
-
# physical file must be readable; missing-file is a usage
|
|
210
|
-
# error too so editors get one consistent failure shape.
|
|
211
|
-
#
|
|
212
|
-
# Returns:
|
|
213
|
-
# - `nil` when neither flag was supplied (legacy path).
|
|
214
|
-
# - `Rigor::Analysis::BufferBinding` when the pair is valid.
|
|
215
|
-
# - `:usage_error` after writing one diagnostic to stderr;
|
|
216
|
-
# the caller MUST translate this to `EXIT_USAGE`.
|
|
217
|
-
def resolve_buffer_binding(options)
|
|
218
|
-
tmp = options[:tmp_file]
|
|
219
|
-
instead = options[:instead_of]
|
|
220
|
-
return nil if tmp.nil? && instead.nil?
|
|
221
|
-
|
|
222
|
-
if tmp.nil? || instead.nil?
|
|
223
|
-
@err.puts("--tmp-file and --instead-of must appear together")
|
|
224
|
-
return :usage_error
|
|
225
|
-
end
|
|
226
|
-
|
|
227
|
-
unless File.file?(tmp)
|
|
228
|
-
@err.puts("--tmp-file #{tmp.inspect}: no such file or not readable")
|
|
229
|
-
return :usage_error
|
|
230
|
-
end
|
|
231
|
-
|
|
232
|
-
Analysis::BufferBinding.new(logical_path: instead, physical_path: tmp)
|
|
233
|
-
end
|
|
234
|
-
|
|
235
|
-
# ADR-15 Phase 4c — resolves the worker count by
|
|
236
|
-
# precedence: CLI `--workers=N` (most explicit) > env
|
|
237
|
-
# `RIGOR_RACTOR_WORKERS` > config `.rigor.yml`
|
|
238
|
-
# `parallel.workers:` > 0 (sequential default). Returns
|
|
239
|
-
# an Integer; non-numeric values raise so typos fail
|
|
240
|
-
# loudly. CLI / env may pass a negative value — clamped
|
|
241
|
-
# to 0 (sequential) so a stray `-1` doesn't crash the
|
|
242
|
-
# pool spawn loop.
|
|
243
|
-
def resolve_workers(options, configuration)
|
|
244
|
-
cli_value = options[:workers]
|
|
245
|
-
return [Integer(cli_value), 0].max if cli_value
|
|
246
|
-
|
|
247
|
-
env_value = ENV.fetch("RIGOR_RACTOR_WORKERS", nil)
|
|
248
|
-
return [Integer(env_value), 0].max if env_value && !env_value.empty?
|
|
249
|
-
|
|
250
|
-
configuration.parallel_workers
|
|
251
|
-
end
|
|
252
|
-
|
|
253
|
-
def parse_check_options # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
|
254
|
-
options = {
|
|
255
|
-
# `nil` triggers `Configuration.discover` (`.rigor.yml` then
|
|
256
|
-
# `.rigor.dist.yml`); an explicit `--config=PATH` overrides.
|
|
257
|
-
config: nil,
|
|
258
|
-
format: "text",
|
|
259
|
-
explain: false,
|
|
260
|
-
cache_stats: false,
|
|
261
|
-
clear_cache: false,
|
|
262
|
-
no_cache: false,
|
|
263
|
-
# Run-stats summary (target files, RBS class universe
|
|
264
|
-
# breakdown, wall time, peak RSS) is on by default
|
|
265
|
-
# because collection is ~free (single syscall for RSS,
|
|
266
|
-
# one walk of `class_decl_paths` for the breakdown).
|
|
267
|
-
# `--no-stats` suppresses it for callers that want a
|
|
268
|
-
# diagnostic-only output stream.
|
|
269
|
-
stats: true,
|
|
270
|
-
# ADR-15 Phase 4c — when nil, falls back to
|
|
271
|
-
# `RIGOR_RACTOR_WORKERS` then `.rigor.yml`
|
|
272
|
-
# `parallel.workers:` then 0 (sequential). See
|
|
273
|
-
# `resolve_workers` for the precedence chain.
|
|
274
|
-
workers: nil,
|
|
275
|
-
# Editor mode (`docs/design/20260516-editor-mode.md`).
|
|
276
|
-
# Both must appear together; the runner uses the pair
|
|
277
|
-
# to bind an in-flight buffer file to its logical path.
|
|
278
|
-
tmp_file: nil,
|
|
279
|
-
instead_of: nil,
|
|
280
|
-
# ADR-22 — baseline filter. `:unset` means "fall through
|
|
281
|
-
# to `.rigor.yml`'s `baseline:` key"; a String overrides
|
|
282
|
-
# the config; `false` (from `--no-baseline`) suppresses
|
|
283
|
-
# any baseline that the config might name.
|
|
284
|
-
baseline: :unset,
|
|
285
|
-
# ADR-22 slice 5 — `--baseline-strict` CI gate: fail the
|
|
286
|
-
# run on any baseline drift, in either direction.
|
|
287
|
-
baseline_strict: false,
|
|
288
|
-
# ADR-32 WD10 carry-over — `--treat-all-as-inline-rbs`
|
|
289
|
-
# forces the `rigor-rbs-inline` plugin into the loaded
|
|
290
|
-
# plugin set with `require_magic_comment: false` so a
|
|
291
|
-
# single ad-hoc `rigor check` invocation treats every
|
|
292
|
-
# analysed file as inline-RBS without the user editing
|
|
293
|
-
# `.rigor.yml`. Intended for single-file / ad-hoc CI use;
|
|
294
|
-
# ordinary projects should configure the plugin in
|
|
295
|
-
# `.rigor.yml`.
|
|
296
|
-
treat_all_as_inline_rbs: false
|
|
297
|
-
}
|
|
298
|
-
parser = OptionParser.new do |opts| # rubocop:disable Metrics/BlockLength
|
|
299
|
-
opts.banner = "Usage: rigor check [options] [paths]"
|
|
300
|
-
opts.on("--config=PATH", "Path to the Rigor configuration file") { |value| options[:config] = value }
|
|
301
|
-
opts.on("--format=FORMAT", "Output format: text or json") { |value| options[:format] = value }
|
|
302
|
-
opts.on("--explain", "Surface fail-soft fallback events as :info diagnostics") { options[:explain] = true }
|
|
303
|
-
opts.on("--cache-stats", "Print on-disk cache inventory at end of run") { options[:cache_stats] = true }
|
|
304
|
-
opts.on("--clear-cache", "Remove the .rigor/cache directory before running") { options[:clear_cache] = true }
|
|
305
|
-
opts.on("--no-cache", "Disable the persistent cache for this run") { options[:no_cache] = true }
|
|
306
|
-
opts.on("--[no-]stats",
|
|
307
|
-
"Print run summary (files, classes, memory, wall time) to stderr (default: on)") do |value|
|
|
308
|
-
options[:stats] = value
|
|
309
|
-
end
|
|
310
|
-
opts.on("--workers=N", Integer,
|
|
311
|
-
"Dispatch per-file analysis across N Ractor workers (default: 0; sequential)") do |value|
|
|
312
|
-
options[:workers] = value
|
|
313
|
-
end
|
|
314
|
-
opts.on("--tmp-file=PATH",
|
|
315
|
-
"Editor mode: read source bytes from PATH instead of --instead-of (paired)") do |value|
|
|
316
|
-
options[:tmp_file] = value
|
|
317
|
-
end
|
|
318
|
-
opts.on("--instead-of=PATH",
|
|
319
|
-
"Editor mode: the logical project path the buffer represents (paired with --tmp-file)") do |value|
|
|
320
|
-
options[:instead_of] = value
|
|
321
|
-
end
|
|
322
|
-
opts.on("--baseline=PATH",
|
|
323
|
-
"ADR-22: load baseline from PATH (overrides .rigor.yml `baseline:`)") do |value|
|
|
324
|
-
options[:baseline] = value
|
|
325
|
-
end
|
|
326
|
-
opts.on("--no-baseline",
|
|
327
|
-
"ADR-22: ignore any configured baseline for this run") do
|
|
328
|
-
options[:baseline] = false
|
|
329
|
-
end
|
|
330
|
-
opts.on("--baseline-strict",
|
|
331
|
-
"ADR-22: fail the run on any baseline drift (CI gate)") do
|
|
332
|
-
options[:baseline_strict] = true
|
|
333
|
-
end
|
|
334
|
-
opts.on("--treat-all-as-inline-rbs",
|
|
335
|
-
"ADR-32: force-load rigor-rbs-inline with require_magic_comment: false") do
|
|
336
|
-
options[:treat_all_as_inline_rbs] = true
|
|
337
|
-
end
|
|
338
|
-
end
|
|
339
|
-
parser.parse!(@argv)
|
|
340
|
-
options
|
|
341
|
-
end
|
|
342
|
-
|
|
343
|
-
# ADR-32 WD10 carry-over — wraps `Configuration.load` so the
|
|
344
|
-
# CLI's `--treat-all-as-inline-rbs` flag can inject a
|
|
345
|
-
# `rigor-rbs-inline` plugin entry with
|
|
346
|
-
# `require_magic_comment: false` into the loaded plugin
|
|
347
|
-
# set. Re-runs the include-aware YAML load and applies the
|
|
348
|
-
# injection before `Configuration.new` so the new entry
|
|
349
|
-
# follows the normal coercion path. A pre-existing
|
|
350
|
-
# `rigor-rbs-inline` entry (by gem name or `id: rbs-inline`)
|
|
351
|
-
# is removed first so the synthesised entry's
|
|
352
|
-
# `require_magic_comment: false` wins unconditionally.
|
|
353
|
-
def load_check_configuration(options)
|
|
354
|
-
return Configuration.load(options.fetch(:config)) unless options.fetch(:treat_all_as_inline_rbs)
|
|
355
|
-
|
|
356
|
-
path = options.fetch(:config) || Configuration.discover
|
|
357
|
-
data = path && File.exist?(path) ? Configuration.load_with_includes(path) : {}
|
|
358
|
-
data = data.dup
|
|
359
|
-
data["plugins"] = inject_treat_all_as_inline_rbs(Array(data["plugins"]))
|
|
360
|
-
Configuration.new(Configuration::DEFAULTS.merge(data))
|
|
361
|
-
end
|
|
362
|
-
|
|
363
|
-
def inject_treat_all_as_inline_rbs(entries)
|
|
364
|
-
filtered = entries.reject { |entry| rigor_rbs_inline_entry?(entry) }
|
|
365
|
-
filtered + [{
|
|
366
|
-
"gem" => "rigor-rbs-inline",
|
|
367
|
-
"id" => "rbs-inline",
|
|
368
|
-
"config" => { "require_magic_comment" => false }
|
|
369
|
-
}]
|
|
370
|
-
end
|
|
371
|
-
|
|
372
|
-
def rigor_rbs_inline_entry?(entry)
|
|
373
|
-
case entry
|
|
374
|
-
when String
|
|
375
|
-
entry == "rigor-rbs-inline"
|
|
376
|
-
when Hash
|
|
377
|
-
string_keyed = entry.to_h { |k, v| [k.to_s, v] }
|
|
378
|
-
string_keyed["gem"] == "rigor-rbs-inline" || string_keyed["id"] == "rbs-inline"
|
|
379
|
-
else
|
|
380
|
-
false
|
|
381
|
-
end
|
|
382
|
-
end
|
|
383
|
-
|
|
384
|
-
def handle_clear_cache(cache_root)
|
|
385
|
-
if File.directory?(cache_root)
|
|
386
|
-
FileUtils.rm_rf(cache_root)
|
|
387
|
-
@out.puts("Cleared cache: #{cache_root}")
|
|
388
|
-
else
|
|
389
|
-
@out.puts("Cache already empty: #{cache_root}")
|
|
390
|
-
end
|
|
391
|
-
end
|
|
392
|
-
|
|
393
|
-
# Emits the {Analysis::RunStats} summary to STDERR so it
|
|
394
|
-
# doesn't interleave with the diagnostic stream (text or
|
|
395
|
-
# JSON) on STDOUT. JSON consumers can pipe stdout cleanly;
|
|
396
|
-
# interactive users still see the summary on their tty.
|
|
397
|
-
def write_run_stats(stats)
|
|
398
|
-
@err.puts("")
|
|
399
|
-
stats.format(@err)
|
|
400
|
-
end
|
|
401
|
-
|
|
402
|
-
# Opt-in developer diagnostics printed after the run: the
|
|
403
|
-
# inference-cutoff trace (RIGOR_BUDGET_TRACE) and the heap-attribution
|
|
404
|
-
# profile (RIGOR_HEAP_PROFILE). Each gates itself, so this is a no-op
|
|
405
|
-
# on a normal run.
|
|
406
|
-
def write_trace_appendices
|
|
407
|
-
write_budget_trace
|
|
408
|
-
write_heap_profile
|
|
409
|
-
end
|
|
84
|
+
require_relative "cli/check_command"
|
|
410
85
|
|
|
411
|
-
|
|
412
|
-
# These are the hard-coded "budget" guards that silently degrade
|
|
413
|
-
# to `Dynamic[top]` / a fallback bound — counting them shows where
|
|
414
|
-
# inference actually stopped. Process-global counters: meaningful
|
|
415
|
-
# only on a single-process run (`--workers 0`), since they do not
|
|
416
|
-
# cross fork boundaries.
|
|
417
|
-
def write_budget_trace
|
|
418
|
-
return unless Inference::BudgetTrace.enabled?
|
|
419
|
-
|
|
420
|
-
counts = Inference::BudgetTrace.snapshot
|
|
421
|
-
@err.puts("")
|
|
422
|
-
@err.puts("Inference cutoffs (RIGOR_BUDGET_TRACE; --workers 0 for an exact count)")
|
|
423
|
-
@err.puts(" recursion-guard hits: #{counts[Inference::BudgetTrace::RECURSION_GUARD]}")
|
|
424
|
-
@err.puts(" ancestor-walk-limit hits: #{counts[Inference::BudgetTrace::ANCESTOR_WALK_LIMIT]}")
|
|
425
|
-
@err.puts(" hkt-fuel-exhausted hits: #{counts[Inference::BudgetTrace::HKT_FUEL_EXHAUSTED]}")
|
|
426
|
-
write_budget_distributions
|
|
427
|
-
end
|
|
428
|
-
|
|
429
|
-
# Dumps the read-only size distributions (ADR-41 Slice 2a). These
|
|
430
|
-
# observe how large unions actually get, with no cap enforced — the
|
|
431
|
-
# data the `union_size` budget default should be chosen from. The
|
|
432
|
-
# `over` thresholds bracket the TypeProf prior (10) and Rigor's spec
|
|
433
|
-
# default (24).
|
|
434
|
-
def write_budget_distributions
|
|
435
|
-
summary = Inference::BudgetTrace.summarize(Inference::BudgetTrace::UNION_ARITY, over: [10, 24, 40])
|
|
436
|
-
pct = summary[:percentiles]
|
|
437
|
-
@err.puts(" union arity: n=#{summary[:count]} max=#{summary[:max]} " \
|
|
438
|
-
"p50=#{pct[:p50]} p90=#{pct[:p90]} p99=#{pct[:p99]}")
|
|
439
|
-
over = summary[:over]
|
|
440
|
-
@err.puts(" unions ≥10: #{over[10]} ≥24: #{over[24]} ≥40: #{over[40]}")
|
|
441
|
-
end
|
|
442
|
-
|
|
443
|
-
# Dumps a live-heap class breakdown (RIGOR_HEAP_PROFILE) — retained
|
|
444
|
-
# objects by class after a forced GC, ranked by total memsize. The
|
|
445
|
-
# tool for attributing where the analyzer's resident memory goes
|
|
446
|
-
# (ADR-41 Slice 2b): it answers whether the heap is type carriers,
|
|
447
|
-
# RBS objects, Prism nodes, or fact-store Hashes/Strings. Walking the
|
|
448
|
-
# whole heap is slow — a dev probe, not a normal diagnostic. Run
|
|
449
|
-
# single-process (`--workers 0`) so the parent heap is the analysis
|
|
450
|
-
# heap; the gem is required lazily so a normal run never loads it.
|
|
451
|
-
def write_heap_profile
|
|
452
|
-
return if ENV["RIGOR_HEAP_PROFILE"].to_s.empty?
|
|
453
|
-
|
|
454
|
-
by_class, total = tally_live_heap
|
|
455
|
-
@err.puts("")
|
|
456
|
-
@err.puts("Heap profile (RIGOR_HEAP_PROFILE; live objects after GC, by class)")
|
|
457
|
-
@err.puts(" total tracked: #{heap_mb(total)} across #{by_class.size} classes")
|
|
458
|
-
by_class.sort_by { |_, (_, bytes)| -bytes }.first(30).each do |name, (count, bytes)|
|
|
459
|
-
@err.puts(" #{heap_mb(bytes).rjust(10)} #{count.to_s.rjust(9)} obj #{name}")
|
|
460
|
-
end
|
|
461
|
-
write_string_allocation_sites
|
|
462
|
-
end
|
|
463
|
-
|
|
464
|
-
# Loads the analysis-path dependencies lazily (so non-check commands
|
|
465
|
-
# stay light) and starts heap-allocation tracing if requested, before
|
|
466
|
-
# any analysis object is allocated.
|
|
467
|
-
def load_check_dependencies
|
|
468
|
-
require_relative "analysis/runner"
|
|
469
|
-
require_relative "analysis/buffer_binding"
|
|
470
|
-
require_relative "analysis/baseline"
|
|
471
|
-
require_relative "cache/store"
|
|
472
|
-
start_heap_trace_if_requested
|
|
473
|
-
end
|
|
474
|
-
|
|
475
|
-
# Starts allocation tracing (RIGOR_HEAP_TRACE) as early as possible so
|
|
476
|
-
# the heap profile can attribute retained Strings to their allocation
|
|
477
|
-
# `file:line`. Very high overhead — run on a small file subset only.
|
|
478
|
-
def start_heap_trace_if_requested
|
|
479
|
-
return if ENV["RIGOR_HEAP_TRACE"].to_s.empty?
|
|
480
|
-
|
|
481
|
-
require "objspace"
|
|
482
|
-
ObjectSpace.trace_object_allocations_start
|
|
483
|
-
end
|
|
484
|
-
|
|
485
|
-
# When RIGOR_HEAP_TRACE is on, groups the live String objects by their
|
|
486
|
-
# allocation site (`sourcefile:sourceline`) and prints the top sites by
|
|
487
|
-
# count — pinpointing which engine code retains the millions of strings
|
|
488
|
-
# that dominate the large-app heap (ADR-41 Slice 2b). Strings allocated
|
|
489
|
-
# before tracing started report `(pre-trace)`.
|
|
490
|
-
def write_string_allocation_sites
|
|
491
|
-
return if ENV["RIGOR_HEAP_TRACE"].to_s.empty?
|
|
492
|
-
|
|
493
|
-
by_site = Hash.new(0)
|
|
494
|
-
ObjectSpace.each_object(String) do |str|
|
|
495
|
-
file = ObjectSpace.allocation_sourcefile(str)
|
|
496
|
-
line = ObjectSpace.allocation_sourceline(str)
|
|
497
|
-
by_site[file ? "#{file}:#{line}" : "(pre-trace)"] += 1
|
|
498
|
-
end
|
|
499
|
-
@err.puts("")
|
|
500
|
-
@err.puts(" String allocation sites (top 25 by live count)")
|
|
501
|
-
by_site.sort_by { |_, n| -n }.first(25).each do |site, n|
|
|
502
|
-
@err.puts(" #{n.to_s.rjust(9)} #{site}")
|
|
503
|
-
end
|
|
504
|
-
end
|
|
505
|
-
|
|
506
|
-
# Walks the whole live heap (after a forced GC) and tallies
|
|
507
|
-
# `{class_name => [count, memsize]}` plus the grand total. Returns
|
|
508
|
-
# `[by_class, total]`. Slow — a dev probe only.
|
|
509
|
-
def tally_live_heap
|
|
510
|
-
require "objspace"
|
|
511
|
-
GC.start
|
|
512
|
-
by_class = Hash.new { |h, k| h[k] = [0, 0] }
|
|
513
|
-
total = 0
|
|
514
|
-
ObjectSpace.each_object do |obj|
|
|
515
|
-
size = ObjectSpace.memsize_of(obj)
|
|
516
|
-
entry = by_class[heap_class_name(obj)]
|
|
517
|
-
entry[0] += 1
|
|
518
|
-
entry[1] += size
|
|
519
|
-
total += size
|
|
520
|
-
end
|
|
521
|
-
[by_class, total]
|
|
522
|
-
end
|
|
523
|
-
|
|
524
|
-
def heap_class_name(obj)
|
|
525
|
-
klass = Object.instance_method(:class).bind_call(obj)
|
|
526
|
-
klass.name || klass.inspect
|
|
527
|
-
rescue StandardError
|
|
528
|
-
"(unknown)"
|
|
529
|
-
end
|
|
530
|
-
|
|
531
|
-
def heap_mb(bytes)
|
|
532
|
-
Kernel.format("%.1f MB", bytes / 1_048_576.0)
|
|
533
|
-
end
|
|
534
|
-
|
|
535
|
-
def write_cache_stats(cache_root, runtime_store)
|
|
536
|
-
inv = Cache::Store.disk_inventory(root: cache_root)
|
|
537
|
-
|
|
538
|
-
@out.puts("")
|
|
539
|
-
@out.puts("Cache (root: #{inv.fetch(:root)})")
|
|
540
|
-
schema = inv.fetch(:schema_version)
|
|
541
|
-
@out.puts(" schema_version: #{schema.nil? ? 'absent' : schema}")
|
|
542
|
-
write_disk_inventory(inv)
|
|
543
|
-
write_runtime_stats(runtime_store) if runtime_store
|
|
544
|
-
end
|
|
545
|
-
|
|
546
|
-
def write_disk_inventory(inv)
|
|
547
|
-
if inv.fetch(:total_entries).zero?
|
|
548
|
-
@out.puts(" (empty)")
|
|
549
|
-
return
|
|
550
|
-
end
|
|
551
|
-
|
|
552
|
-
@out.puts(" #{inv.fetch(:total_entries)} entries, #{format_bytes(inv.fetch(:total_bytes))}")
|
|
553
|
-
inv.fetch(:producers).each do |producer|
|
|
554
|
-
bytes = format_bytes(producer.fetch(:bytes))
|
|
555
|
-
@out.puts(" #{producer.fetch(:id)}: #{producer.fetch(:entries)} entries, #{bytes}")
|
|
556
|
-
end
|
|
557
|
-
end
|
|
558
|
-
|
|
559
|
-
def write_runtime_stats(store)
|
|
560
|
-
stats = store.stats
|
|
561
|
-
hits = stats.fetch(:hits)
|
|
562
|
-
misses = stats.fetch(:misses)
|
|
563
|
-
writes = stats.fetch(:writes)
|
|
564
|
-
@out.puts(" this run: #{hits} #{plural(hits, 'hit')}, " \
|
|
565
|
-
"#{misses} #{plural(misses, 'miss', 'misses')}, " \
|
|
566
|
-
"#{writes} #{plural(writes, 'write')}")
|
|
567
|
-
stats.fetch(:by_producer).each do |id, counts|
|
|
568
|
-
@out.puts(" #{id}: #{counts.fetch(:hits)} #{plural(counts.fetch(:hits), 'hit')}, " \
|
|
569
|
-
"#{counts.fetch(:misses)} #{plural(counts.fetch(:misses), 'miss', 'misses')}, " \
|
|
570
|
-
"#{counts.fetch(:writes)} #{plural(counts.fetch(:writes), 'write')}")
|
|
571
|
-
end
|
|
572
|
-
end
|
|
573
|
-
|
|
574
|
-
def plural(count, singular, plural = "#{singular}s")
|
|
575
|
-
count == 1 ? singular : plural
|
|
576
|
-
end
|
|
577
|
-
|
|
578
|
-
def format_bytes(bytes)
|
|
579
|
-
return "#{bytes} B" if bytes < 1024
|
|
580
|
-
return format("%.1f KiB", bytes / 1024.0) if bytes < 1024 * 1024
|
|
581
|
-
|
|
582
|
-
format("%.1f MiB", bytes / (1024.0 * 1024.0))
|
|
86
|
+
CheckCommand.new(argv: @argv, out: @out, err: @err).run
|
|
583
87
|
end
|
|
584
88
|
|
|
585
89
|
def run_init
|
|
@@ -696,6 +200,12 @@ module Rigor
|
|
|
696
200
|
TypeOfCommand.new(argv: @argv, out: @out, err: @err).run
|
|
697
201
|
end
|
|
698
202
|
|
|
203
|
+
def run_trace
|
|
204
|
+
require_relative "cli/trace_command"
|
|
205
|
+
|
|
206
|
+
TraceCommand.new(argv: @argv, out: @out, err: @err).run
|
|
207
|
+
end
|
|
208
|
+
|
|
699
209
|
def run_type_scan
|
|
700
210
|
require_relative "cli/type_scan_command"
|
|
701
211
|
|
|
@@ -779,34 +289,6 @@ module Rigor
|
|
|
779
289
|
CLI::PluginCommand.new(argv: @argv, out: @out, err: @err).run
|
|
780
290
|
end
|
|
781
291
|
|
|
782
|
-
def write_result(result, format)
|
|
783
|
-
case format
|
|
784
|
-
when "json"
|
|
785
|
-
@out.puts(JSON.pretty_generate(result.to_h))
|
|
786
|
-
when "text"
|
|
787
|
-
write_text_result(result)
|
|
788
|
-
else
|
|
789
|
-
raise OptionParser::InvalidArgument, "unsupported format: #{format}"
|
|
790
|
-
end
|
|
791
|
-
end
|
|
792
|
-
|
|
793
|
-
# Text output adds a one-line summary so users see the
|
|
794
|
-
# diagnostic-count immediately. The summary distinguishes
|
|
795
|
-
# the success and failure cases and reports the affected
|
|
796
|
-
# file count for failures.
|
|
797
|
-
def write_text_result(result)
|
|
798
|
-
result.diagnostics.each { |diagnostic| @out.puts(diagnostic) }
|
|
799
|
-
|
|
800
|
-
if result.success?
|
|
801
|
-
@out.puts("No diagnostics") if result.diagnostics.empty?
|
|
802
|
-
return
|
|
803
|
-
end
|
|
804
|
-
|
|
805
|
-
error_files = result.diagnostics.select(&:error?).map(&:path).uniq.size
|
|
806
|
-
@out.puts("")
|
|
807
|
-
@out.puts("#{result.error_count} error(s) in #{error_files} file(s)")
|
|
808
|
-
end
|
|
809
|
-
|
|
810
292
|
def help
|
|
811
293
|
<<~HELP
|
|
812
294
|
Usage: rigor <command> [options]
|
|
@@ -816,6 +298,7 @@ module Rigor
|
|
|
816
298
|
init Create a starter .rigor.yml
|
|
817
299
|
annotate Print FILE with each line's last-expression type
|
|
818
300
|
type-of Print the inferred type at FILE:LINE:COL
|
|
301
|
+
trace Replay how the engine typed FILE as a terminal animation
|
|
819
302
|
type-scan Report Scope#type_of coverage across PATHs
|
|
820
303
|
explain Print the description of one or all CheckRules
|
|
821
304
|
diff Compare current diagnostics to a saved baseline JSON
|
|
@@ -77,7 +77,7 @@ module Rigor
|
|
|
77
77
|
return new([]) if data.nil?
|
|
78
78
|
raise ArgumentError, "dependencies: must be a Hash, got #{data.inspect}" unless data.is_a?(Hash)
|
|
79
79
|
|
|
80
|
-
raw_entries =
|
|
80
|
+
raw_entries = coerce_source_inference(data["source_inference"])
|
|
81
81
|
entries, warnings = dedupe_entries(raw_entries)
|
|
82
82
|
budget = coerce_budget_per_gem(data.fetch("budget_per_gem", DEFAULT_BUDGET_PER_GEM))
|
|
83
83
|
strategy = coerce_budget_overrun_strategy(
|
|
@@ -158,6 +158,23 @@ module Rigor
|
|
|
158
158
|
|
|
159
159
|
private
|
|
160
160
|
|
|
161
|
+
# `source_inference:` is a list of per-gem entries, or `false` /
|
|
162
|
+
# omitted to disable it (the default). Guard the Ruby
|
|
163
|
+
# `Array(false) == [false]` quirk that would otherwise feed `false`
|
|
164
|
+
# straight into coerce_entry and crash the whole run on a perfectly
|
|
165
|
+
# reasonable "off" config (`dependencies: { source_inference: false }`).
|
|
166
|
+
def coerce_source_inference(value)
|
|
167
|
+
return [] if value.nil? || value == false
|
|
168
|
+
|
|
169
|
+
unless value.is_a?(Array)
|
|
170
|
+
raise ArgumentError,
|
|
171
|
+
"dependencies.source_inference: must be a list of entries " \
|
|
172
|
+
"(or false / omitted to disable), got #{value.inspect}"
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
value.map { |raw| coerce_entry(raw) }
|
|
176
|
+
end
|
|
177
|
+
|
|
161
178
|
def coerce_entry(raw)
|
|
162
179
|
unless raw.is_a?(Hash)
|
|
163
180
|
raise ArgumentError,
|