rigortype 0.2.8 → 0.3.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/README.md +2 -2
- data/data/core_overlay/csv.rbs +28 -0
- data/data/core_overlay/psych.rbs +22 -0
- data/docs/handbook/01-getting-started.md +9 -1
- data/docs/handbook/02-everyday-types.md +4 -1
- data/docs/handbook/03-narrowing.md +2 -1
- data/docs/handbook/04-tuples-and-shapes.md +7 -3
- data/docs/handbook/06-classes.md +18 -9
- data/docs/handbook/08-understanding-errors.md +4 -3
- data/docs/handbook/11-sig-gen.md +11 -1
- data/docs/handbook/appendix-mypy.md +4 -3
- data/docs/install.md +14 -6
- data/docs/manual/01-installation.md +99 -4
- data/docs/manual/02-cli-reference.md +65 -19
- data/docs/manual/03-configuration.md +34 -2
- data/docs/manual/04-diagnostics.md +23 -0
- data/docs/manual/07-plugins.md +5 -5
- data/docs/manual/09-editor-integration.md +4 -4
- data/docs/manual/11-ci.md +51 -0
- data/docs/manual/12-caching.md +65 -0
- data/docs/manual/14-rails-quickstart.md +4 -2
- data/docs/manual/15-type-protection-coverage.md +21 -0
- data/docs/manual/plugins/rigor-actionpack.md +1 -1
- data/docs/manual/plugins/rigor-activerecord.md +12 -5
- data/docs/manual/plugins/rigor-rails-routes.md +11 -0
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +9 -1
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +5 -3
- data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +128 -0
- data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +60 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -2
- data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +117 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +125 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -1
- data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +283 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +10 -1
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +93 -0
- data/lib/rigor/analysis/check_rules.rb +557 -95
- data/lib/rigor/analysis/dependency_recorder.rb +93 -9
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/incremental_session.rb +456 -51
- data/lib/rigor/analysis/path_expansion.rb +42 -0
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +188 -0
- data/lib/rigor/analysis/rule_catalog.rb +226 -1
- data/lib/rigor/analysis/run_cache_key.rb +68 -0
- data/lib/rigor/analysis/run_cache_probe.rb +72 -0
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +181 -28
- data/lib/rigor/analysis/runner/pool_coordinator.rb +77 -15
- data/lib/rigor/analysis/runner/project_pre_passes.rb +95 -49
- data/lib/rigor/analysis/runner/run_snapshots.rb +9 -2
- data/lib/rigor/analysis/runner.rb +343 -68
- data/lib/rigor/analysis/severity_stamp.rb +43 -0
- data/lib/rigor/analysis/worker_session.rb +30 -2
- data/lib/rigor/bleeding_edge.rb +37 -8
- data/lib/rigor/builtins/regex_refinement.rb +36 -1
- data/lib/rigor/cache/descriptor.rb +88 -28
- data/lib/rigor/cache/file_digest.rb +158 -0
- data/lib/rigor/cache/incremental_snapshot.rb +41 -5
- data/lib/rigor/cache/rbs_descriptor.rb +56 -10
- data/lib/rigor/cache/store.rb +207 -60
- data/lib/rigor/ci_detector.rb +90 -0
- data/lib/rigor/cli/annotate_command.rb +15 -9
- data/lib/rigor/cli/baseline_command.rb +4 -1
- data/lib/rigor/cli/check_command.rb +212 -27
- data/lib/rigor/cli/coverage_command.rb +56 -30
- data/lib/rigor/cli/diagnostic_formats.rb +4 -1
- data/lib/rigor/cli/docs_command.rb +0 -30
- data/lib/rigor/cli/doctor_command.rb +163 -23
- data/lib/rigor/cli/lsp_command.rb +5 -0
- data/lib/rigor/cli/mcp_command.rb +5 -0
- data/lib/rigor/cli/plugins_command.rb +36 -6
- data/lib/rigor/cli/plugins_renderer.rb +44 -12
- data/lib/rigor/cli/probe_environment.rb +85 -0
- data/lib/rigor/cli/protection_fork_scan.rb +55 -0
- data/lib/rigor/cli/protection_report.rb +7 -1
- data/lib/rigor/cli/sig_gen_command.rb +36 -9
- data/lib/rigor/cli/skill_command.rb +1 -32
- data/lib/rigor/cli/trace_command.rb +5 -6
- data/lib/rigor/cli/type_of_command.rb +9 -8
- data/lib/rigor/cli/type_scan_command.rb +8 -8
- data/lib/rigor/cli.rb +15 -4
- data/lib/rigor/config_audit.rb +48 -2
- data/lib/rigor/configuration/severity_profile.rb +31 -0
- data/lib/rigor/configuration.rb +156 -8
- data/lib/rigor/environment/default_libraries.rb +35 -0
- data/lib/rigor/environment/missing_gem_constant_index.rb +128 -0
- data/lib/rigor/environment/rbs_loader.rb +164 -5
- data/lib/rigor/environment.rb +71 -43
- data/lib/rigor/inference/budget_trace.rb +77 -2
- data/lib/rigor/inference/closure_escape_analyzer.rb +14 -1
- data/lib/rigor/inference/def_handle.rb +23 -0
- data/lib/rigor/inference/def_node_resolver.rb +90 -0
- data/lib/rigor/inference/def_return_typer.rb +2 -1
- data/lib/rigor/inference/expression_typer.rb +379 -113
- data/lib/rigor/inference/fork_map.rb +87 -0
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +30 -5
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +200 -0
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +22 -16
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +33 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +11 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +42 -9
- data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +5 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +28 -35
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +40 -8
- data/lib/rigor/inference/method_dispatcher.rb +57 -1
- data/lib/rigor/inference/mutation_widening.rb +42 -8
- data/lib/rigor/inference/narrowing.rb +245 -17
- data/lib/rigor/inference/parameter_inference_collector.rb +70 -10
- data/lib/rigor/inference/project_patched_scanner.rb +2 -1
- data/lib/rigor/inference/rbs_type_translator.rb +8 -1
- data/lib/rigor/inference/scope_indexer.rb +460 -49
- data/lib/rigor/inference/statement_evaluator.rb +111 -17
- data/lib/rigor/inference/struct_fold_safety.rb +48 -11
- data/lib/rigor/inference/synthetic_method_scanner.rb +15 -14
- data/lib/rigor/inference/void_origin.rb +25 -0
- data/lib/rigor/inference/void_tail_summary.rb +220 -0
- data/lib/rigor/language_server/completion_provider.rb +2 -1
- data/lib/rigor/language_server/document_symbol_provider.rb +2 -1
- data/lib/rigor/language_server/folding_range_provider.rb +2 -1
- data/lib/rigor/language_server/selection_range_provider.rb +2 -1
- data/lib/rigor/language_server/signature_help_provider.rb +2 -1
- data/lib/rigor/plugin/base.rb +9 -22
- data/lib/rigor/plugin/inflector.rb +12 -3
- data/lib/rigor/plugin/io_boundary.rb +5 -1
- data/lib/rigor/plugin/isolation.rb +81 -11
- data/lib/rigor/plugin/load_error.rb +10 -1
- data/lib/rigor/plugin/loader.rb +96 -14
- data/lib/rigor/plugin/node_rule_walk.rb +5 -3
- data/lib/rigor/plugin/registry.rb +32 -23
- data/lib/rigor/plugin.rb +26 -0
- data/lib/rigor/protection/mutator.rb +3 -2
- data/lib/rigor/reflection.rb +64 -0
- data/lib/rigor/runtime/jit.rb +128 -0
- data/lib/rigor/scope/discovery_index.rb +12 -2
- data/lib/rigor/scope.rb +122 -28
- data/lib/rigor/sig_gen/classification.rb +5 -1
- data/lib/rigor/sig_gen/generator.rb +38 -6
- data/lib/rigor/sig_gen/observation_collector.rb +4 -3
- data/lib/rigor/sig_gen/rbs_validity.rb +44 -0
- data/lib/rigor/sig_gen/renderer.rb +10 -0
- data/lib/rigor/sig_gen/write_result.rb +9 -4
- data/lib/rigor/sig_gen/writer.rb +24 -2
- data/lib/rigor/source/node_children.rb +116 -0
- data/lib/rigor/source/node_locator.rb +3 -1
- data/lib/rigor/source/node_walker.rb +4 -2
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/type/difference.rb +28 -24
- data/lib/rigor/type/hash_shape.rb +34 -10
- data/lib/rigor/version.rb +1 -1
- data/lib/rigortype.rb +24 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +4 -2
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +6 -4
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +23 -8
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +5 -3
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +21 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +13 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +65 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +20 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +172 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +22 -8
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +4 -2
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +32 -0
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +38 -6
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -1
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +3 -1
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +10 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +11 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +3 -1
- data/plugins/rigor-rails/lib/rigor-rails.rb +11 -12
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +191 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +3 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +19 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +48 -15
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +42 -12
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +83 -9
- data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +9 -5
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +3 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +11 -0
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +4 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +4 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +3 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +74 -14
- data/sig/prism_node_children.rbs +9 -0
- data/sig/rigor/environment.rbs +1 -0
- data/sig/rigor/inference.rbs +7 -0
- data/sig/rigor/plugin/base.rbs +2 -3
- data/sig/rigor/reflection.rbs +1 -0
- data/sig/rigor/scope.rbs +12 -1
- data/skills/rigor-ci-setup/SKILL.md +10 -0
- data/skills/rigor-editor-setup/SKILL.md +11 -0
- data/skills/rigor-next-steps/SKILL.md +23 -2
- data/skills/rigor-plugin-author/SKILL.md +3 -3
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +2 -2
- data/skills/rigor-plugin-review/SKILL.md +2 -2
- data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +5 -4
- data/skills/rigor-project-init/SKILL.md +29 -3
- data/skills/rigor-project-init/references/01-detect.md +1 -1
- data/skills/rigor-project-init/references/02-configure.md +39 -5
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +20 -0
- data/skills/rigor-project-init/references/05-jit-performance.md +125 -0
- metadata +49 -24
- data/lib/rigor/cli/ci_detector.rb +0 -89
|
@@ -10,9 +10,12 @@ module Rigor
|
|
|
10
10
|
# The two formats carry the same content; JSON is meant for tooling (SKILLs, CI, editor integrations) while text is
|
|
11
11
|
# for interactive inspection. Rows are printed in the order the loader resolved them.
|
|
12
12
|
class PluginsRenderer # rubocop:disable Metrics/ClassLength
|
|
13
|
-
|
|
13
|
+
# `inflection:` (ADR-90) is nil, or `{required_by: [ids], available: Boolean}` — the activation-time
|
|
14
|
+
# `Plugin::Inflector` probe result when a loaded plugin consumes the shared inflection helper.
|
|
15
|
+
def initialize(rows:, configuration_path:, inflection: nil)
|
|
14
16
|
@rows = rows
|
|
15
17
|
@configuration_path = configuration_path
|
|
18
|
+
@inflection = inflection
|
|
16
19
|
end
|
|
17
20
|
|
|
18
21
|
def text
|
|
@@ -25,17 +28,23 @@ module Rigor
|
|
|
25
28
|
end
|
|
26
29
|
lines << ""
|
|
27
30
|
lines << footer
|
|
31
|
+
lines.concat(inflection_warning_lines)
|
|
28
32
|
lines.join("\n")
|
|
29
33
|
end
|
|
30
34
|
|
|
31
35
|
def json
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
payload = {
|
|
37
|
+
"configuration" => @configuration_path,
|
|
38
|
+
"plugins" => @rows.map { |row| row_json(row) },
|
|
39
|
+
"summary" => summary
|
|
40
|
+
}
|
|
41
|
+
unless @inflection.nil?
|
|
42
|
+
payload["inflection"] = {
|
|
43
|
+
"required_by" => @inflection.fetch(:required_by),
|
|
44
|
+
"available" => @inflection.fetch(:available)
|
|
37
45
|
}
|
|
38
|
-
|
|
46
|
+
end
|
|
47
|
+
JSON.pretty_generate(payload)
|
|
39
48
|
end
|
|
40
49
|
|
|
41
50
|
# ADR-37 § "Machine-readable capability catalogue" — the focused per-plugin extension-protocol dump. Only loaded
|
|
@@ -84,7 +93,7 @@ module Rigor
|
|
|
84
93
|
[
|
|
85
94
|
["node_rule", row[:node_rule_types]],
|
|
86
95
|
["dynamic_return receivers", row[:dynamic_return_receivers]],
|
|
87
|
-
["
|
|
96
|
+
["narrowing_facts methods", row[:narrowing_facts_methods]],
|
|
88
97
|
["produces", row[:produces]],
|
|
89
98
|
["consumes", row[:consumes]]
|
|
90
99
|
].filter_map { |label, values| "#{label}: #{values.join(', ')}" if values.any? }
|
|
@@ -97,7 +106,7 @@ module Rigor
|
|
|
97
106
|
"version" => row[:version],
|
|
98
107
|
"node_rule_types" => row[:node_rule_types],
|
|
99
108
|
"dynamic_return_receivers" => row[:dynamic_return_receivers],
|
|
100
|
-
"
|
|
109
|
+
"narrowing_facts_methods" => row[:narrowing_facts_methods],
|
|
101
110
|
"produces" => row[:produces],
|
|
102
111
|
"consumes" => row[:consumes]
|
|
103
112
|
}
|
|
@@ -122,6 +131,22 @@ module Rigor
|
|
|
122
131
|
end
|
|
123
132
|
end
|
|
124
133
|
|
|
134
|
+
# ADR-90 — the text-view degradation warning. Printed only when the probe ran (an Inflector-consuming
|
|
135
|
+
# plugin is loaded) AND inflection is unavailable; the healthy case stays quiet.
|
|
136
|
+
def inflection_warning_lines
|
|
137
|
+
return [] if @inflection.nil? || @inflection.fetch(:available)
|
|
138
|
+
|
|
139
|
+
[
|
|
140
|
+
"",
|
|
141
|
+
"WARNING: ActiveSupport::Inflector is not loadable — inflection-dependent checks of " \
|
|
142
|
+
"#{@inflection.fetch(:required_by).join(', ')} will silently produce no diagnostics.",
|
|
143
|
+
" Rigor found activesupport neither in its own gem environment nor in the project's " \
|
|
144
|
+
"bundler install tree.",
|
|
145
|
+
" Fix: run `bundle install` in the analyzed project (Rigor reads vendor/bundle or " \
|
|
146
|
+
".bundle/config), or install activesupport into Rigor's gem environment."
|
|
147
|
+
]
|
|
148
|
+
end
|
|
149
|
+
|
|
125
150
|
def row_lines(row)
|
|
126
151
|
marker = row[:status] == :loaded ? "OK " : "ERR"
|
|
127
152
|
head = if row[:status] == :loaded
|
|
@@ -131,6 +156,10 @@ module Rigor
|
|
|
131
156
|
end
|
|
132
157
|
lines = [head]
|
|
133
158
|
lines << " #{row[:description]}" if row[:description]
|
|
159
|
+
# #194 slice 1 — the file the plugin gem loaded from, so an engine↔plugin version skew (a stale
|
|
160
|
+
# installed `rigortype` shadowing a checkout's bundled plugin) is a glance, not a bisect. Shown for
|
|
161
|
+
# loaded rows and for a require-succeeded-then-failed error row; omitted when unresolvable.
|
|
162
|
+
lines << " path: #{row[:path]}" if row[:path]
|
|
134
163
|
|
|
135
164
|
if row[:status] == :load_error
|
|
136
165
|
lines << " load error: #{row[:load_error]}"
|
|
@@ -170,8 +199,8 @@ module Rigor
|
|
|
170
199
|
if row[:dynamic_return_receivers].any?
|
|
171
200
|
lines << " dynamic_return receivers: #{row[:dynamic_return_receivers].join(', ')}"
|
|
172
201
|
end
|
|
173
|
-
if row[:
|
|
174
|
-
lines << "
|
|
202
|
+
if row[:narrowing_facts_methods].any?
|
|
203
|
+
lines << " narrowing_facts methods: #{row[:narrowing_facts_methods].join(', ')}"
|
|
175
204
|
end
|
|
176
205
|
lines
|
|
177
206
|
end
|
|
@@ -213,6 +242,9 @@ module Rigor
|
|
|
213
242
|
"id" => row[:id],
|
|
214
243
|
"version" => row[:version],
|
|
215
244
|
"description" => row[:description],
|
|
245
|
+
# #194 slice 1 — additive per-row provenance; null when the resolver could not pin the file (a
|
|
246
|
+
# require failure, or an entry-file basename differing from the gem name).
|
|
247
|
+
"path" => row[:path],
|
|
216
248
|
"config" => row[:config],
|
|
217
249
|
"signature_paths" => row[:signature_paths].map do |sp|
|
|
218
250
|
{ "path" => sp[:path], "exists" => sp[:exists], "rbs_files" => sp[:rbs_files] }
|
|
@@ -231,7 +263,7 @@ module Rigor
|
|
|
231
263
|
"source_rbs_synthesizer" => row[:source_rbs_synthesizer],
|
|
232
264
|
"node_rule_types" => row[:node_rule_types],
|
|
233
265
|
"dynamic_return_receivers" => row[:dynamic_return_receivers],
|
|
234
|
-
"
|
|
266
|
+
"narrowing_facts_methods" => row[:narrowing_facts_methods],
|
|
235
267
|
"load_error" => row[:load_error]
|
|
236
268
|
}
|
|
237
269
|
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../environment"
|
|
4
|
+
require_relative "../plugin"
|
|
5
|
+
require_relative "../plugin/loader"
|
|
6
|
+
require_relative "../plugin/services"
|
|
7
|
+
require_relative "../reflection"
|
|
8
|
+
require_relative "../type/combinator"
|
|
9
|
+
|
|
10
|
+
module Rigor
|
|
11
|
+
class CLI
|
|
12
|
+
# Shared construction of the analysis-observing environment for the single-shot probe commands
|
|
13
|
+
# (`type-of`, `type-scan`, `trace`, `annotate`). Each of these answers "what does the engine infer here?"
|
|
14
|
+
# for a hand-picked file or position, so the environment they type against MUST match the one `rigor check`
|
|
15
|
+
# analyses with — otherwise a probe reports a type the real run never computes.
|
|
16
|
+
#
|
|
17
|
+
# The gap this closes: the probes historically built their environment with
|
|
18
|
+
# `Environment.for_project(libraries:, signature_paths:)` only — no plugin registry, no `source_files:`. The
|
|
19
|
+
# `source_rbs_synthesizer` plugin hook (ADR-32 / ADR-93's auto-wired `rigor-rbs-inline`) therefore never
|
|
20
|
+
# ran there, so a class whose only signature comes from an inline `#: () -> void` annotation typed as
|
|
21
|
+
# `Dynamic[top]` under a probe while `check` resolved it through the synthesized RBS. That divergence
|
|
22
|
+
# misattributed a dispatch tier during the #162 transitive-void design pass (see the 2026-07-19 addendum in
|
|
23
|
+
# docs/adr/100-static-diagnostic-family-and-void-origins.md).
|
|
24
|
+
#
|
|
25
|
+
# This threads the two things that were missing — the plugin registry built by the plugin loader, and the
|
|
26
|
+
# `source_files:` the synthesizers run over — matching `rigor check`'s `Environment.for_project` call
|
|
27
|
+
# (built in `Analysis::Runner::PoolCoordinator#build_runner_environment`). A probe legitimately simplifies
|
|
28
|
+
# relative to check: no synthesis-failure reporter (a probe has no diagnostic pipeline) and no synthesis
|
|
29
|
+
# cache store (a single-position probe recomputes cheaply). Plugin loading itself matches check — same
|
|
30
|
+
# `.rigor.yml` `plugins:` config, same ADR-93 auto-wire (already applied by `Configuration.load`), same
|
|
31
|
+
# `enabled:` / `require_magic_comment` semantics via `Plugin::Loader.load`.
|
|
32
|
+
#
|
|
33
|
+
# Fail-soft is the invariant: a project with no plugins, an unresolvable plugin gem, or any error during the
|
|
34
|
+
# plugin-aware build degrades to the bare environment the probes used before — never a crash. `Plugin::Loader`
|
|
35
|
+
# already isolates per-entry load failures onto the registry; the `rescue` here is the belt-and-suspenders
|
|
36
|
+
# around anything the loader or the plugin-aware env build itself might raise.
|
|
37
|
+
module ProbeEnvironment
|
|
38
|
+
module_function
|
|
39
|
+
|
|
40
|
+
# Builds the plugin-aware {Rigor::Environment} a probe types against.
|
|
41
|
+
#
|
|
42
|
+
# @param configuration [Rigor::Configuration] the loaded project configuration (already carries the
|
|
43
|
+
# ADR-93 auto-wired `rigor-rbs-inline` entry when the library is resolvable).
|
|
44
|
+
# @param source_files [Array<String>] the file(s) the probe inspects. Threaded so each loaded plugin's
|
|
45
|
+
# `source_rbs_synthesizer` runs over them at env-build time; an empty list contributes no synthesized RBS.
|
|
46
|
+
# @return [Rigor::Environment]
|
|
47
|
+
def build(configuration:, source_files:)
|
|
48
|
+
Environment.for_project(
|
|
49
|
+
libraries: configuration.libraries,
|
|
50
|
+
signature_paths: configuration.signature_paths,
|
|
51
|
+
plugin_registry: load_plugin_registry(configuration),
|
|
52
|
+
source_files: source_files
|
|
53
|
+
)
|
|
54
|
+
rescue StandardError
|
|
55
|
+
bare(configuration)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Loads the project's configured plugins the same way the plugin-inspection commands (`rigor plugins`,
|
|
59
|
+
# `rigor doctor`) do: a `Plugin::Services` with no cache store (the probe recomputes) driving
|
|
60
|
+
# `Plugin::Loader.load`. Returns `nil` — so `Environment.for_project` skips the plugin tier entirely —
|
|
61
|
+
# when the project declares no plugins.
|
|
62
|
+
def load_plugin_registry(configuration)
|
|
63
|
+
return nil if configuration.plugins.empty?
|
|
64
|
+
|
|
65
|
+
services = Plugin::Services.new(
|
|
66
|
+
reflection: Reflection,
|
|
67
|
+
type: Type::Combinator,
|
|
68
|
+
configuration: configuration,
|
|
69
|
+
cache_store: nil
|
|
70
|
+
)
|
|
71
|
+
Plugin::Loader.load(configuration: configuration, services: services)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# The pre-fix environment: RBS + project signatures only, no plugin tier and no synthesized RBS. The
|
|
75
|
+
# fail-soft floor — a probe on a project with broken or absent plugin setup lands here and behaves exactly
|
|
76
|
+
# as it did before this parity fix.
|
|
77
|
+
def bare(configuration)
|
|
78
|
+
Environment.for_project(
|
|
79
|
+
libraries: configuration.libraries,
|
|
80
|
+
signature_paths: configuration.signature_paths
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../inference/fork_map"
|
|
6
|
+
|
|
7
|
+
module Rigor
|
|
8
|
+
class CLI
|
|
9
|
+
# Fork-pool for the `rigor coverage --protection` scan (P3-10). The plugin-aware environment and the
|
|
10
|
+
# seeded scope are built ONCE on the parent — the expensive part: RBS env, plugin registry, and the
|
|
11
|
+
# cross-file discovery + parameter-inference seed — and children copy-on-write inherit them (via
|
|
12
|
+
# {Inference::ForkMap}), each analysing a contiguous slice of paths and returning per-path results the
|
|
13
|
+
# parent merges in original path order.
|
|
14
|
+
#
|
|
15
|
+
# The scan is pure-read (no reporter drains, no plugin-emission merge, no prepare diagnostics), so a
|
|
16
|
+
# slice payload is just `{path => result}`. Determinism: the parent merges slice payloads in slice order
|
|
17
|
+
# and the accumulator absorbs in `paths` order, so the fused report is byte-identical to a sequential run
|
|
18
|
+
# regardless of which worker produced which file.
|
|
19
|
+
module ProtectionForkScan
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
# A parse failure for one file, carrying only the marshalable error count (Prism error objects are not
|
|
23
|
+
# reliably marshalable, and the accumulator only needs the count).
|
|
24
|
+
ParseError = Data.define(:count)
|
|
25
|
+
|
|
26
|
+
# @param paths [Array<String>] the files to scan, in caller order.
|
|
27
|
+
# @param scanner [Inference::ProtectionScanner] built on the parent; COW-inherited by workers.
|
|
28
|
+
# @param environment [Rigor::Environment] the scanner's environment, prewarmed here before forking.
|
|
29
|
+
# @param configuration [Rigor::Configuration] for the Prism `target_ruby` version.
|
|
30
|
+
# @param workers [Integer] resolved worker count (≤1 → sequential).
|
|
31
|
+
# @return [Hash{String => Inference::ProtectionScanner::FileResult, ParseError}] one entry per path.
|
|
32
|
+
def run(paths:, scanner:, environment:, configuration:, workers:)
|
|
33
|
+
# Force the full RBS load on the parent so children copy-on-write inherit a warm environment rather
|
|
34
|
+
# than each rebuilding it after the fork (mirrors the check fork pool's parent-side prewarm). A
|
|
35
|
+
# no-op on the sequential path but cheap.
|
|
36
|
+
environment.rbs_loader&.prewarm if Inference::ForkMap.parallel?([workers, paths.size].min)
|
|
37
|
+
|
|
38
|
+
payloads = Inference::ForkMap.call(items: paths, workers: workers) do |slice|
|
|
39
|
+
slice.to_h { |path| [path, scan_path(path, scanner, configuration)] }
|
|
40
|
+
end
|
|
41
|
+
payloads.each_with_object({}) { |slice_results, merged| merged.merge!(slice_results) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Parses one file and returns the scanner's {Inference::ProtectionScanner::FileResult}, or a
|
|
45
|
+
# {ParseError} when the source does not parse.
|
|
46
|
+
def scan_path(path, scanner, configuration)
|
|
47
|
+
source = File.read(path)
|
|
48
|
+
parse_result = Prism.parse(source, filepath: path, version: configuration.target_ruby)
|
|
49
|
+
return ParseError.new(count: parse_result.errors.size) if parse_result.errors.any?
|
|
50
|
+
|
|
51
|
+
scanner.scan(parse_result.value)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -86,7 +86,13 @@ module Rigor
|
|
|
86
86
|
end
|
|
87
87
|
|
|
88
88
|
def record_parse_error(path, errors)
|
|
89
|
-
|
|
89
|
+
record_parse_error_count(path, errors.size)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Count-based variant for the fork-pool path, where a worker carries only the marshalable error count
|
|
93
|
+
# (see {ProtectionForkScan::ParseError}), not the Prism error objects.
|
|
94
|
+
def record_parse_error_count(path, count)
|
|
95
|
+
@parse_errors << { "path" => path, "errors" => count }
|
|
90
96
|
end
|
|
91
97
|
|
|
92
98
|
def to_report
|
|
@@ -42,21 +42,43 @@ module Rigor
|
|
|
42
42
|
paths = @argv.empty? ? configuration.paths : @argv
|
|
43
43
|
|
|
44
44
|
observations = collect_observations(configuration, options)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
generator = SigGen::Generator.new(configuration: configuration, paths: paths,
|
|
46
|
+
observations: observations,
|
|
47
|
+
include_private: options.fetch(:include_private))
|
|
48
|
+
candidates = generator.run
|
|
48
49
|
mode = options.fetch(:mode).to_sym
|
|
49
50
|
|
|
50
|
-
if mode == :write
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
51
|
+
status = if mode == :write
|
|
52
|
+
dispatch_write(candidates, configuration, options)
|
|
53
|
+
else
|
|
54
|
+
dispatch_print_or_diff(candidates, mode, options)
|
|
55
|
+
0
|
|
56
|
+
end
|
|
57
|
+
report_unrenderable(generator.unrenderable)
|
|
58
|
+
status
|
|
56
59
|
end
|
|
57
60
|
|
|
58
61
|
private
|
|
59
62
|
|
|
63
|
+
# A method whose rendered RBS does not parse is a Rigor rendering defect, not a fact about the user's
|
|
64
|
+
# code — the generator skipped it (so the rest of the signatures are still usable and still valid), but
|
|
65
|
+
# staying silent would leave the user with a quietly incomplete `sig/` and us with an unreported bug.
|
|
66
|
+
# Reported on stderr so it never contaminates `--print` output being piped into a file.
|
|
67
|
+
def report_unrenderable(unrenderable)
|
|
68
|
+
return if unrenderable.empty?
|
|
69
|
+
|
|
70
|
+
@err.puts(
|
|
71
|
+
"rigor sig-gen: skipped #{unrenderable.size} method(s) whose generated RBS does not parse. " \
|
|
72
|
+
"This is a bug in Rigor's RBS rendering, not in your code — please report it. " \
|
|
73
|
+
"The remaining signatures are unaffected."
|
|
74
|
+
)
|
|
75
|
+
unrenderable.each do |method|
|
|
76
|
+
@err.puts(" #{method.path}: #{method.class_name}##{method.method_name}")
|
|
77
|
+
@err.puts(" rendered: #{method.rbs}")
|
|
78
|
+
@err.puts(" #{method.error}")
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
60
82
|
def dispatch_print_or_diff(candidates, mode, options)
|
|
61
83
|
SigGen::Renderer.new(out: @out).render(
|
|
62
84
|
candidates: candidates,
|
|
@@ -66,6 +88,7 @@ module Rigor
|
|
|
66
88
|
)
|
|
67
89
|
end
|
|
68
90
|
|
|
91
|
+
# @return [Integer] exit status — non-zero when a file the user asked to write could not be written.
|
|
69
92
|
def dispatch_write(candidates, configuration, options)
|
|
70
93
|
layout_index = SigGen::LayoutIndex.new(signature_paths: configuration.signature_paths)
|
|
71
94
|
path_mapper = SigGen::PathMapper.new(configuration: configuration, layout_index: layout_index)
|
|
@@ -74,6 +97,10 @@ module Rigor
|
|
|
74
97
|
results = writer.write_all(candidates)
|
|
75
98
|
|
|
76
99
|
SigGen::Renderer.new(out: @out).render_write(results: results, format: options.fetch(:format))
|
|
100
|
+
# An assembled file that does not parse is refused rather than written. The user asked for a write and
|
|
101
|
+
# did not get one, so the command must not report success — a green `sig-gen --write` in CI would
|
|
102
|
+
# otherwise mean nothing.
|
|
103
|
+
results.any? { |result| result.action == :skipped_invalid_rbs } ? 1 : 0
|
|
77
104
|
end
|
|
78
105
|
|
|
79
106
|
# Slice 3 — collect call-site argument observations when `--params=observed` is set. When `--observe=PATH` is not
|
|
@@ -37,9 +37,7 @@ module Rigor
|
|
|
37
37
|
# the SKILL. Also spelled `describe`, and
|
|
38
38
|
# surfaced top-level as `rigor describe`.
|
|
39
39
|
#
|
|
40
|
-
#
|
|
41
|
-
# deprecation notice; they are removed in v0.3.0 (see docs/ROADMAP.md § "Scheduled CLI deprecations"). `describe` is
|
|
42
|
-
# a no-argument action, not a name-slot verb, so it stays first-class alongside `--describe`.
|
|
40
|
+
# `describe` is a no-argument action, not a name-slot verb, so it stays first-class alongside `--describe`.
|
|
43
41
|
class SkillCommand < Command
|
|
44
42
|
USAGE = <<~USAGE
|
|
45
43
|
Usage: rigor skill [<name>] [--full <name>] [--path <name>] [--list] [--describe]
|
|
@@ -60,30 +58,14 @@ module Rigor
|
|
|
60
58
|
rigor skill --full rigor-baseline-reduce
|
|
61
59
|
rigor skill --path rigor-baseline-reduce
|
|
62
60
|
rigor skill --describe (also: rigor describe)
|
|
63
|
-
|
|
64
|
-
Deprecated (removed in v0.3.0) — use the forms above:
|
|
65
|
-
rigor skill list -> rigor skill --list
|
|
66
|
-
rigor skill print <name> -> rigor skill <name>
|
|
67
|
-
rigor skill path <name> -> rigor skill --path <name>
|
|
68
61
|
USAGE
|
|
69
62
|
|
|
70
63
|
# The bundled skills live at `<gem_root>/skills/`. From `lib/rigor/cli/skill_command.rb` that is three directories
|
|
71
64
|
# up.
|
|
72
65
|
SKILLS_ROOT = File.expand_path("../../../skills", __dir__)
|
|
73
66
|
|
|
74
|
-
# The verb subcommands the flags superseded keep working with a stderr deprecation notice until this version drops
|
|
75
|
-
# them. Each maps to the canonical advice printed and the flag it rewrites to.
|
|
76
|
-
LEGACY_VERB_REMOVAL = "v0.3.0"
|
|
77
|
-
LEGACY_VERBS = {
|
|
78
|
-
"list" => { old: "list", advice: "--list", flag: "--list" },
|
|
79
|
-
"print" => { old: "print <name>", advice: "<name>", flag: "--print" },
|
|
80
|
-
"path" => { old: "path <name>", advice: "--path <name>", flag: "--path" }
|
|
81
|
-
}.freeze
|
|
82
|
-
|
|
83
67
|
# @return [Integer] CLI exit status.
|
|
84
68
|
def run
|
|
85
|
-
rewrite_legacy_verb!
|
|
86
|
-
|
|
87
69
|
case @argv.first
|
|
88
70
|
when nil
|
|
89
71
|
run_list
|
|
@@ -112,19 +94,6 @@ module Rigor
|
|
|
112
94
|
|
|
113
95
|
private
|
|
114
96
|
|
|
115
|
-
# Translate a deprecated verb spelling into its flag form, warning once on stderr, so the dispatch above only
|
|
116
|
-
# handles canonical forms.
|
|
117
|
-
def rewrite_legacy_verb!
|
|
118
|
-
spec = LEGACY_VERBS[@argv.first]
|
|
119
|
-
return unless spec
|
|
120
|
-
|
|
121
|
-
@err.puts(
|
|
122
|
-
"rigor skill: `#{spec.fetch(:old)}` is deprecated and will be removed in " \
|
|
123
|
-
"#{LEGACY_VERB_REMOVAL}; use `rigor skill #{spec.fetch(:advice)}` instead."
|
|
124
|
-
)
|
|
125
|
-
@argv[0] = spec.fetch(:flag)
|
|
126
|
-
end
|
|
127
|
-
|
|
128
97
|
def run_list
|
|
129
98
|
skills = discover_skills
|
|
130
99
|
if skills.empty?
|
|
@@ -12,6 +12,7 @@ require_relative "../inference/flow_tracer"
|
|
|
12
12
|
require_relative "../inference/scope_indexer"
|
|
13
13
|
require_relative "command"
|
|
14
14
|
require_relative "trace_renderer"
|
|
15
|
+
require_relative "probe_environment"
|
|
15
16
|
|
|
16
17
|
module Rigor
|
|
17
18
|
class CLI
|
|
@@ -90,13 +91,11 @@ module Rigor
|
|
|
90
91
|
0
|
|
91
92
|
end
|
|
92
93
|
|
|
93
|
-
# Mirrors the single-file path `rigor type-of` takes:
|
|
94
|
-
#
|
|
94
|
+
# Mirrors the single-file path `rigor type-of` takes: the plugin-aware environment (so a trace replays the
|
|
95
|
+
# same dispatch `rigor check` would, including plugin-synthesized RBS — see {ProbeEnvironment}), an empty
|
|
96
|
+
# seed scope, one statement-level evaluation of the whole program — but recorded under the FlowTracer.
|
|
95
97
|
def record_events(root, file, configuration)
|
|
96
|
-
environment =
|
|
97
|
-
libraries: configuration.libraries,
|
|
98
|
-
signature_paths: configuration.signature_paths
|
|
99
|
-
)
|
|
98
|
+
environment = ProbeEnvironment.build(configuration: configuration, source_files: [file])
|
|
100
99
|
scope = Scope.empty(environment: environment, source_path: file)
|
|
101
100
|
Inference::FlowTracer.record { scope.evaluate(root) }
|
|
102
101
|
end
|
|
@@ -13,6 +13,7 @@ require_relative "../inference/scope_indexer"
|
|
|
13
13
|
require_relative "type_of_renderer"
|
|
14
14
|
require_relative "command"
|
|
15
15
|
require_relative "options"
|
|
16
|
+
require_relative "probe_environment"
|
|
16
17
|
|
|
17
18
|
module Rigor
|
|
18
19
|
class CLI
|
|
@@ -92,14 +93,14 @@ module Rigor
|
|
|
92
93
|
0
|
|
93
94
|
end
|
|
94
95
|
|
|
95
|
-
# Builds
|
|
96
|
-
#
|
|
97
|
-
#
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
)
|
|
96
|
+
# Builds the plugin-aware environment relative to the probed file, so the reported type matches what `rigor
|
|
97
|
+
# check` computes for the same position — including types synthesized from inline RBS annotations by the
|
|
98
|
+
# ADR-93 auto-wired `rigor-rbs-inline` plugin (see {ProbeEnvironment} for the #162 misattribution this
|
|
99
|
+
# closes). The probed file is threaded as the synthesizer's `source_files:`. Project-RBS auto-detection
|
|
100
|
+
# roots at CWD today; future work will walk parent directories to find the enclosing `Gemfile`/`*.gemspec`
|
|
101
|
+
# so probes against files outside the current process's CWD still see the right `sig/` tree.
|
|
102
|
+
def project_environment(file, configuration)
|
|
103
|
+
ProbeEnvironment.build(configuration: configuration, source_files: [file])
|
|
103
104
|
end
|
|
104
105
|
|
|
105
106
|
def file_exists?(file)
|
|
@@ -11,6 +11,7 @@ require_relative "../scope"
|
|
|
11
11
|
require_relative "type_scan_renderer"
|
|
12
12
|
require_relative "type_scan_report"
|
|
13
13
|
require_relative "command"
|
|
14
|
+
require_relative "probe_environment"
|
|
14
15
|
|
|
15
16
|
module Rigor
|
|
16
17
|
class CLI
|
|
@@ -69,20 +70,19 @@ module Rigor
|
|
|
69
70
|
|
|
70
71
|
def scan_paths(paths, options)
|
|
71
72
|
configuration = Configuration.load(options.fetch(:config))
|
|
72
|
-
scope = Scope.empty(environment: project_environment(configuration))
|
|
73
|
+
scope = Scope.empty(environment: project_environment(configuration, paths))
|
|
73
74
|
scanner = Inference::CoverageScanner.new(scope: scope)
|
|
74
75
|
accumulator = ScanAccumulator.new
|
|
75
76
|
paths.each { |path| scan_one(path, scanner, accumulator, configuration) }
|
|
76
77
|
accumulator.to_report(paths, options)
|
|
77
78
|
end
|
|
78
79
|
|
|
79
|
-
# Builds
|
|
80
|
-
# `libraries:` / `signature_paths:` keys when present.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
)
|
|
80
|
+
# Builds the plugin-aware environment that auto-detects `<cwd>/sig` by default and honours the
|
|
81
|
+
# configuration's `libraries:` / `signature_paths:` keys when present. The scanned `paths` are threaded as
|
|
82
|
+
# the plugin `source_rbs_synthesizer` inputs so coverage reflects the same synthesized RBS `rigor check`
|
|
83
|
+
# sees (see {ProbeEnvironment}).
|
|
84
|
+
def project_environment(configuration, source_files)
|
|
85
|
+
ProbeEnvironment.build(configuration: configuration, source_files: source_files)
|
|
86
86
|
end
|
|
87
87
|
|
|
88
88
|
def scan_one(path, scanner, accumulator, configuration)
|
data/lib/rigor/cli.rb
CHANGED
|
@@ -11,7 +11,7 @@ require_relative "analysis/diagnostic"
|
|
|
11
11
|
require_relative "analysis/result"
|
|
12
12
|
require_relative "cli/options"
|
|
13
13
|
require_relative "cli/diagnostic_formats"
|
|
14
|
-
require_relative "
|
|
14
|
+
require_relative "ci_detector"
|
|
15
15
|
|
|
16
16
|
module Rigor
|
|
17
17
|
# The CLI class is a dispatcher: each `run_*` method delegates to a command-specific class once the command grows
|
|
@@ -20,6 +20,16 @@ module Rigor
|
|
|
20
20
|
class CLI # rubocop:disable Metrics/ClassLength
|
|
21
21
|
EXIT_USAGE = 64
|
|
22
22
|
|
|
23
|
+
# The published location of `schemas/rigor-config.schema.json`, written into the `.rigor.yml` that
|
|
24
|
+
# `rigor init` generates so an editor validates the file as the user types.
|
|
25
|
+
#
|
|
26
|
+
# This MUST equal the schema's own `$id`, and it MUST resolve. Both are gated
|
|
27
|
+
# (`spec/rigor/config_schema_spec.rb`) because neither failure has a symptom: an unreachable schema
|
|
28
|
+
# is indistinguishable from a schema with no complaints, so a stale URL here silently disables
|
|
29
|
+
# validation for every project that ran `rigor init`. It pointed at a nonexistent org until
|
|
30
|
+
# 2026-07-17 and nothing noticed. See `docs/internal-spec/config.md`.
|
|
31
|
+
CONFIG_SCHEMA_URL = "https://github.com/rigortype/rigor/raw/master/schemas/rigor-config.schema.json"
|
|
32
|
+
|
|
23
33
|
HANDLERS = {
|
|
24
34
|
"check" => :run_check,
|
|
25
35
|
"init" => :run_init,
|
|
@@ -138,9 +148,10 @@ module Rigor
|
|
|
138
148
|
# most likely to want to edit.
|
|
139
149
|
def init_template
|
|
140
150
|
<<~YAML
|
|
141
|
-
# yaml-language-server: $schema
|
|
142
|
-
# Rigor configuration.
|
|
143
|
-
#
|
|
151
|
+
# yaml-language-server: $schema=#{CONFIG_SCHEMA_URL}
|
|
152
|
+
# Rigor configuration. The full key reference is
|
|
153
|
+
# https://github.com/rigortype/rigor/blob/master/docs/manual/03-configuration.md
|
|
154
|
+
# (or run `rigor docs configuration`).
|
|
144
155
|
#
|
|
145
156
|
# Keys you may want to edit:
|
|
146
157
|
# - target_ruby: minimum Ruby version your project targets.
|
data/lib/rigor/config_audit.rb
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "did_you_mean"
|
|
4
|
+
|
|
3
5
|
require_relative "signature_path_audit"
|
|
4
|
-
|
|
6
|
+
# ADR-87 WD4 — only the pure rule-id table (`ALL_RULES` / `RULE_FAMILIES`) is needed, so require the light
|
|
7
|
+
# `rule_ids.rb` rather than the engine-pulling `check_rules.rb`; keeps `config_audit` (loaded by every check)
|
|
8
|
+
# off the inference engine so the boot-slimming hit path stays engine-free.
|
|
9
|
+
require_relative "analysis/check_rules/rule_ids"
|
|
5
10
|
|
|
6
11
|
module Rigor
|
|
7
12
|
# Audits a loaded {Configuration} for the class of mistake where a configured value
|
|
@@ -36,12 +41,53 @@ module Rigor
|
|
|
36
41
|
# paths resolve against (the CLI's CWD), used only by the explicit-path checks.
|
|
37
42
|
# @return [Array<Warning>]
|
|
38
43
|
def self.warnings(configuration, project_root: Dir.pwd)
|
|
39
|
-
|
|
44
|
+
unknown_key_warnings(configuration) +
|
|
45
|
+
signature_path_warnings(configuration) +
|
|
40
46
|
library_warnings(configuration) +
|
|
41
47
|
rule_token_warnings(configuration) +
|
|
42
48
|
explicit_path_warnings(configuration, project_root)
|
|
43
49
|
end
|
|
44
50
|
|
|
51
|
+
# Top-level keys the loader does not own. The archetypal case is a typo — `excludee:` for
|
|
52
|
+
# `exclude:` — which the loader drops in silence, so the exclusion never applies and the run
|
|
53
|
+
# reports errors from the very files the user meant to skip, with nothing anywhere saying the key
|
|
54
|
+
# did nothing. That is exactly this module's class of mistake, and it was the one whole-key
|
|
55
|
+
# instance of it going unwarned.
|
|
56
|
+
#
|
|
57
|
+
# A reserved namespace ({Configuration::RESERVED_NAMESPACES}) is unknown *on purpose* — another
|
|
58
|
+
# implementation reads it from the same file — so it is exempt. {Configuration#unknown_keys}
|
|
59
|
+
# already applies that exemption; warning here would push users to delete the key their other
|
|
60
|
+
# tool needs.
|
|
61
|
+
#
|
|
62
|
+
# **Top level only, and deliberately.** A nested check would have to name each group's known keys,
|
|
63
|
+
# and DEFAULTS cannot supply them: `DEFAULTS["dependencies"]` carries `source_inference` and
|
|
64
|
+
# `budget_per_gem` but not `budget_overrun_strategy`, which is real (`Configuration::Dependencies`
|
|
65
|
+
# reads it), documented, and schema-declared — so a DEFAULTS-keyed nested check would flag a
|
|
66
|
+
# working config. `severity_overrides:` is an open map of rule ids besides. Nested unknown keys are
|
|
67
|
+
# the schema tier's job: every nested object in `schemas/rigor-config.schema.json` is
|
|
68
|
+
# `additionalProperties: false`, and `config_schema_spec` keeps it complete
|
|
69
|
+
# ([ADR-99](../../docs/adr/99-config-schema-authority.md), `docs/internal-spec/config.md`).
|
|
70
|
+
def self.unknown_key_warnings(configuration)
|
|
71
|
+
configuration.unknown_keys.map do |key|
|
|
72
|
+
suggestion = suggest_config_key(key)
|
|
73
|
+
hint = suggestion ? " Did you mean `#{suggestion}`?" : ""
|
|
74
|
+
Warning.new(
|
|
75
|
+
kind: :unknown_key,
|
|
76
|
+
message: "`#{key}` is not a recognized configuration key; it has no effect.#{hint}",
|
|
77
|
+
fields: { "key" => key, "suggestion" => suggestion }
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Nearest known key, or nil. Uses `DidYouMean::SpellChecker` — the engine behind Ruby's own
|
|
83
|
+
# `NoMethodError` hints, and the one `Plugin::Base.suggest` wraps. Called directly rather than
|
|
84
|
+
# through that helper because `plugin/base.rb` pulls Prism and the diagnostic model in, and
|
|
85
|
+
# ADR-87 WD4 keeps this file (loaded by every check) off that path; `did_you_mean` is stdlib.
|
|
86
|
+
def self.suggest_config_key(key)
|
|
87
|
+
DidYouMean::SpellChecker.new(dictionary: Configuration::KNOWN_KEYS).correct(key.to_s).first
|
|
88
|
+
end
|
|
89
|
+
private_class_method :suggest_config_key
|
|
90
|
+
|
|
45
91
|
# `signature_paths:` entries that resolve to nothing — delegated to {SignaturePathAudit},
|
|
46
92
|
# which mirrors the loader's `directory?` + recursive `**/*.rbs` acceptance test.
|
|
47
93
|
def self.signature_path_warnings(configuration)
|