rigortype 0.2.9 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +2 -2
- data/data/builtins/ruby_core/array.yml +416 -392
- data/data/builtins/ruby_core/file.yml +42 -42
- data/data/builtins/ruby_core/hash.yml +302 -302
- data/data/builtins/ruby_core/io.yml +191 -191
- data/data/builtins/ruby_core/numeric.yml +321 -366
- data/data/builtins/ruby_core/proc.yml +124 -124
- data/data/builtins/ruby_core/range.yml +21 -21
- data/data/builtins/ruby_core/rational.yml +39 -39
- data/data/builtins/ruby_core/re.yml +65 -65
- data/data/builtins/ruby_core/set.yml +106 -106
- data/data/builtins/ruby_core/struct.yml +14 -14
- data/data/core_overlay/string_scanner.rbs +6 -5
- data/docs/handbook/01-getting-started.md +22 -34
- data/docs/handbook/03-narrowing.md +2 -1
- data/docs/handbook/04-tuples-and-shapes.md +7 -3
- data/docs/handbook/06-classes.md +19 -10
- data/docs/handbook/07-rbs-and-extended.md +76 -101
- data/docs/handbook/08-understanding-errors.md +114 -246
- data/docs/handbook/09-plugins.md +54 -144
- data/docs/handbook/11-sig-gen.md +11 -1
- data/docs/handbook/README.md +5 -3
- data/docs/handbook/appendix-liskov.md +4 -2
- data/docs/handbook/appendix-mypy.md +4 -3
- data/docs/handbook/appendix-phpstan.md +14 -7
- data/docs/handbook/appendix-steep.md +4 -2
- data/docs/handbook/appendix-type-theory.md +3 -1
- data/docs/install.md +14 -6
- data/docs/manual/01-installation.md +98 -4
- data/docs/manual/02-cli-reference.md +81 -12
- data/docs/manual/03-configuration.md +34 -2
- data/docs/manual/04-diagnostics.md +59 -4
- data/docs/manual/06-baseline.md +35 -1
- data/docs/manual/07-plugins.md +4 -4
- data/docs/manual/08-skills.md +6 -1
- data/docs/manual/09-editor-integration.md +7 -6
- data/docs/manual/11-ci.md +51 -0
- data/docs/manual/12-caching.md +65 -0
- data/docs/manual/plugins/rigor-actioncable.md +32 -0
- data/docs/manual/plugins/rigor-devise.md +4 -2
- 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 +112 -0
- data/lib/rigor/analysis/check_rules.rb +581 -98
- 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 +78 -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 +344 -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_cache_producer.rb +11 -1
- data/lib/rigor/cache/rbs_descriptor.rb +56 -10
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +38 -0
- data/lib/rigor/cache/store.rb +132 -27
- 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 +223 -32
- data/lib/rigor/cli/check_invocation.rb +84 -0
- data/lib/rigor/cli/coverage_command.rb +6 -1
- data/lib/rigor/cli/docs_command.rb +0 -30
- data/lib/rigor/cli/doctor_command.rb +169 -31
- 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/sig_gen_command.rb +36 -9
- data/lib/rigor/cli/skill_command.rb +22 -33
- data/lib/rigor/cli/skill_deep_probe.rb +172 -0
- data/lib/rigor/cli/skill_describe.rb +75 -9
- 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 +36 -0
- data/lib/rigor/environment/rbs_loader.rb +164 -5
- data/lib/rigor/environment.rb +29 -28
- 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 +361 -113
- 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 +17 -1
- 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 +127 -17
- data/lib/rigor/inference/parameter_inference_collector.rb +15 -0
- 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 +451 -36
- data/lib/rigor/inference/statement_evaluator.rb +64 -15
- 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/buffer_resolution.rb +6 -3
- data/lib/rigor/language_server/buffer_table.rb +46 -6
- data/lib/rigor/language_server/completion_provider.rb +2 -1
- data/lib/rigor/language_server/diagnostic_publisher.rb +4 -0
- 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/incremental_sync.rb +159 -0
- data/lib/rigor/language_server/selection_range_provider.rb +2 -1
- data/lib/rigor/language_server/server.rb +19 -9
- data/lib/rigor/language_server/signature_help_provider.rb +2 -1
- data/lib/rigor/language_server.rb +1 -0
- data/lib/rigor/plugin/base.rb +38 -24
- 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 +207 -49
- 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-actioncable/lib/rigor/plugin/actioncable.rb +51 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +6 -4
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +5 -3
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +3 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +4 -2
- 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-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 +3 -1
- 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/routes_parser.rb +7 -5
- 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/cache.rbs +6 -0
- data/sig/rigor/inference/void_origin.rbs +18 -0
- data/sig/rigor/inference.rbs +7 -0
- data/sig/rigor/plugin/base.rbs +6 -6
- 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 +48 -26
- data/lib/rigor/cli/ci_detector.rb +0 -89
|
@@ -12,7 +12,7 @@ require_relative "../plugin/loader"
|
|
|
12
12
|
require_relative "../plugin/services"
|
|
13
13
|
require_relative "../reflection"
|
|
14
14
|
require_relative "../type/combinator"
|
|
15
|
-
require_relative "
|
|
15
|
+
require_relative "check_invocation"
|
|
16
16
|
require_relative "command"
|
|
17
17
|
require_relative "options"
|
|
18
18
|
|
|
@@ -34,6 +34,8 @@ module Rigor
|
|
|
34
34
|
CHECK_PLUGINS = "plugins"
|
|
35
35
|
CHECK_BASELINE = "baseline"
|
|
36
36
|
CHECK_RAILS = "rails_plugins"
|
|
37
|
+
CHECK_GEMFILE = "gemfile_install"
|
|
38
|
+
CHECK_PLUGIN_SKEW = "plugin_skew"
|
|
37
39
|
|
|
38
40
|
RAILS_LOCK_MARKERS = %w[railties actionpack activerecord actioncable].freeze
|
|
39
41
|
RAILS_PLUGIN_MARKERS = %w[
|
|
@@ -51,20 +53,19 @@ module Rigor
|
|
|
51
53
|
findings.concat(audit_config(configuration))
|
|
52
54
|
|
|
53
55
|
# 2. Run a scoped analysis to gather stats + diagnostics for the
|
|
54
|
-
# deeper checks
|
|
55
|
-
|
|
56
|
+
# deeper checks, through the shared check-invocation entry point (#148).
|
|
57
|
+
# `READ_ONLY_OPTIONS` is no-cache so the probe doesn't churn disk.
|
|
58
|
+
result = CheckInvocation.run(
|
|
56
59
|
configuration: configuration,
|
|
57
|
-
options:
|
|
58
|
-
|
|
59
|
-
cache_root: configuration.cache_path
|
|
60
|
-
)
|
|
61
|
-
result = runner.run(configuration.paths)
|
|
60
|
+
options: CheckInvocation::READ_ONLY_OPTIONS
|
|
61
|
+
).result
|
|
62
62
|
|
|
63
63
|
# 3. RBS environment check.
|
|
64
64
|
findings.concat(check_rbs_environment(result))
|
|
65
65
|
|
|
66
|
-
# 4. Plugin load check.
|
|
67
|
-
|
|
66
|
+
# 4. Plugin load check + 8. installation-skew check share one loaded registry.
|
|
67
|
+
registry = load_plugin_registry(configuration)
|
|
68
|
+
findings.concat(check_plugins(registry))
|
|
68
69
|
|
|
69
70
|
# 5. Baseline drift check.
|
|
70
71
|
findings.concat(check_baseline(configuration, result))
|
|
@@ -72,6 +73,12 @@ module Rigor
|
|
|
72
73
|
# 6. Rails locked but no Rails plugins.
|
|
73
74
|
findings.concat(check_rails_plugins(configuration))
|
|
74
75
|
|
|
76
|
+
# 7. Rigor itself resolved as a project dependency.
|
|
77
|
+
findings.concat(check_gemfile_install)
|
|
78
|
+
|
|
79
|
+
# 8. A bundled plugin loaded from a different rigortype installation than the engine (#194 slice 3).
|
|
80
|
+
findings.concat(check_plugin_skew(registry))
|
|
81
|
+
|
|
75
82
|
report(findings, options.fetch(:format))
|
|
76
83
|
findings.any? { |f| f[:status] == :fail } ? 1 : 0
|
|
77
84
|
rescue OptionParser::InvalidArgument => e
|
|
@@ -122,37 +129,67 @@ module Rigor
|
|
|
122
129
|
stats = result.stats
|
|
123
130
|
return [] unless stats
|
|
124
131
|
|
|
132
|
+
quarantined = quarantined_signature_diagnostics(result)
|
|
125
133
|
if stats.rbs_classes_total.zero?
|
|
126
|
-
[
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
status: :fail,
|
|
130
|
-
message: "RBS environment is empty (#{stats.rbs_classes_total} classes available)",
|
|
131
|
-
hint: "The RBS environment failed to build or loaded no signatures. " \
|
|
132
|
-
"Check `signature_paths:` for duplicate declarations, or run " \
|
|
133
|
-
"`rbs collection install` for gem signatures."
|
|
134
|
-
}
|
|
135
|
-
]
|
|
134
|
+
[empty_rbs_env_finding(stats)]
|
|
135
|
+
elsif quarantined.any?
|
|
136
|
+
[degraded_rbs_env_finding(stats, quarantined)]
|
|
136
137
|
else
|
|
137
|
-
[
|
|
138
|
-
{
|
|
139
|
-
check: CHECK_RBS,
|
|
140
|
-
status: :pass,
|
|
141
|
-
message: "RBS environment healthy (#{stats.rbs_classes_total} classes available)",
|
|
142
|
-
hint: nil
|
|
143
|
-
}
|
|
144
|
-
]
|
|
138
|
+
[healthy_rbs_env_finding(stats)]
|
|
145
139
|
end
|
|
146
140
|
end
|
|
147
141
|
|
|
148
|
-
def
|
|
142
|
+
def empty_rbs_env_finding(stats)
|
|
143
|
+
{
|
|
144
|
+
check: CHECK_RBS,
|
|
145
|
+
status: :fail,
|
|
146
|
+
message: "RBS environment is empty (#{stats.rbs_classes_total} classes available)",
|
|
147
|
+
hint: "The RBS environment failed to build or loaded no signatures. " \
|
|
148
|
+
"Check `signature_paths:` for duplicate declarations, or run " \
|
|
149
|
+
"`rbs collection install` for gem signatures."
|
|
150
|
+
}
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# The env is non-empty precisely BECAUSE the broken file was quarantined, so the class count alone reads
|
|
154
|
+
# as healthy — this is the case doctor used to pass. Classify the diagnostic the run already produced
|
|
155
|
+
# (ADR-77: route existing evidence) rather than re-probing the sig tree.
|
|
156
|
+
def degraded_rbs_env_finding(stats, quarantined)
|
|
157
|
+
{
|
|
158
|
+
check: CHECK_RBS,
|
|
159
|
+
status: :warn,
|
|
160
|
+
message: "RBS environment degraded — #{quarantined.size} `signature_paths:` file(s) " \
|
|
161
|
+
"skipped, #{stats.rbs_classes_total} classes available",
|
|
162
|
+
hint: "An unparseable `.rbs` is skipped so the rest of the env survives, which makes the " \
|
|
163
|
+
"run quieter, not cleaner: the types it declares are gone. Run `rbs validate` on your " \
|
|
164
|
+
"`sig/` set and fix the parse error. To make this fail the build, opt into the " \
|
|
165
|
+
"`reject-unparseable-signatures` bleeding-edge feature."
|
|
166
|
+
}
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def healthy_rbs_env_finding(stats)
|
|
170
|
+
{
|
|
171
|
+
check: CHECK_RBS,
|
|
172
|
+
status: :pass,
|
|
173
|
+
message: "RBS environment healthy (#{stats.rbs_classes_total} classes available)",
|
|
174
|
+
hint: nil
|
|
175
|
+
}
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def quarantined_signature_diagnostics(result)
|
|
179
|
+
result.diagnostics.select { |diagnostic| diagnostic.rule == "rbs.coverage.quarantined-signature" }
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def load_plugin_registry(configuration)
|
|
149
183
|
services = Plugin::Services.new(
|
|
150
184
|
reflection: Reflection,
|
|
151
185
|
type: Type::Combinator,
|
|
152
186
|
configuration: configuration,
|
|
153
187
|
cache_store: nil
|
|
154
188
|
)
|
|
155
|
-
|
|
189
|
+
Plugin::Loader.load(configuration: configuration, services: services)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def check_plugins(registry)
|
|
156
193
|
errors = registry.load_errors
|
|
157
194
|
return [] if errors.empty?
|
|
158
195
|
|
|
@@ -166,6 +203,52 @@ module Rigor
|
|
|
166
203
|
]
|
|
167
204
|
end
|
|
168
205
|
|
|
206
|
+
# WD5 (#194 slice 3) — a bundled plugin should load from the engine's own `plugins/` tree; slice 2
|
|
207
|
+
# anchors it there. This guards the residual anchoring cannot see: a bundled plugin whose resolved file
|
|
208
|
+
# sits OUTSIDE the engine tree — the fallback-require path, or a genuinely mixed installation where a
|
|
209
|
+
# foreign copy was already loaded. That is the #194 skew, which ran the engine with a load-bearing
|
|
210
|
+
# false-positive gate silently missing. Only plugins the engine bundles are checked; a third-party /
|
|
211
|
+
# project-bundle plugin is not the engine's to vouch for and is never flagged, and a nil resolved path
|
|
212
|
+
# (an injected requirer, an in-process no-op load) carries no provenance and is skipped.
|
|
213
|
+
def check_plugin_skew(registry)
|
|
214
|
+
registry.resolved_gem_paths.filter_map do |gem_name, resolved_path|
|
|
215
|
+
next if resolved_path.nil?
|
|
216
|
+
next unless Plugin::Loader.bundled_plugin_path(gem_name)
|
|
217
|
+
next if inside_engine_tree?(resolved_path)
|
|
218
|
+
|
|
219
|
+
plugin_skew_finding(gem_name, resolved_path)
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def plugin_skew_finding(gem_name, resolved_path)
|
|
224
|
+
{
|
|
225
|
+
check: CHECK_PLUGIN_SKEW,
|
|
226
|
+
status: :warn,
|
|
227
|
+
message: "Plugin #{gem_name} loaded from a different rigortype installation than the engine " \
|
|
228
|
+
"(loaded from #{resolved_path}; engine at #{Plugin::Loader::ENGINE_ROOT})",
|
|
229
|
+
hint: "The engine and its bundled plugins are versioned together, so a copy resolved from another " \
|
|
230
|
+
"installation can run the engine with a mismatched plugin (the cause of wrong diagnostics in " \
|
|
231
|
+
"#194). Ensure a single `rigortype` is on the load path — remove any separately-installed " \
|
|
232
|
+
"`rigortype` gem that shadows this checkout, or reinstall so the engine and its `plugins/` " \
|
|
233
|
+
"tree come from one place."
|
|
234
|
+
}
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# True when `resolved_path` lives inside the engine's own tree. Both sides are resolved through
|
|
238
|
+
# `File.realpath` first so a symlinked gem home / checkout (e.g. macOS `/var` → `/private/var`) does not
|
|
239
|
+
# read as foreign; a vanished file degrades to `File.expand_path` rather than raising.
|
|
240
|
+
def inside_engine_tree?(resolved_path)
|
|
241
|
+
engine = real_or_expanded(Plugin::Loader::ENGINE_ROOT)
|
|
242
|
+
target = real_or_expanded(resolved_path)
|
|
243
|
+
target == engine || target.start_with?("#{engine}#{File::SEPARATOR}")
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def real_or_expanded(path)
|
|
247
|
+
File.realpath(path)
|
|
248
|
+
rescue StandardError
|
|
249
|
+
File.expand_path(path)
|
|
250
|
+
end
|
|
251
|
+
|
|
169
252
|
def check_baseline(configuration, result)
|
|
170
253
|
path = configuration.baseline_path
|
|
171
254
|
return [] if path.nil? || !File.exist?(path)
|
|
@@ -238,6 +321,52 @@ module Rigor
|
|
|
238
321
|
false
|
|
239
322
|
end
|
|
240
323
|
|
|
324
|
+
# Rigor is a tool, not a library (ADR-27): resolving it as one of the project's own dependencies pins the
|
|
325
|
+
# application to Rigor's Ruby and drags Rigor's dependency graph into the app's resolution. The gem carries
|
|
326
|
+
# guardrails against arriving here, but they only fire during install and at boot — a project that already
|
|
327
|
+
# made the mistake never sees them again, so doctor names the state.
|
|
328
|
+
def check_gemfile_install
|
|
329
|
+
lock = File.join(Dir.pwd, "Gemfile.lock")
|
|
330
|
+
return [] unless File.file?(lock)
|
|
331
|
+
return [] unless rubygems_sourced_rigortype?(lock)
|
|
332
|
+
|
|
333
|
+
[
|
|
334
|
+
{
|
|
335
|
+
check: CHECK_GEMFILE,
|
|
336
|
+
status: :fail,
|
|
337
|
+
message: "Rigor is resolved as a dependency of this project (`rigortype` under GEM in Gemfile.lock)",
|
|
338
|
+
hint: "Remove `rigortype` from the Gemfile and install Rigor on its own — see " \
|
|
339
|
+
"https://github.com/rigortype/rigor/blob/master/docs/install.md. To keep the version " \
|
|
340
|
+
"pinned, use `mise use --pin gem:rigortype`, or an isolated `BUNDLE_GEMFILE` holding only " \
|
|
341
|
+
"Rigor (the CI chapter's pattern), which leaves your application's resolution untouched."
|
|
342
|
+
}
|
|
343
|
+
]
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# True only when `rigortype` resolves from a GEM remote — i.e. an ordinary `gem "rigortype"` in the app's
|
|
347
|
+
# Gemfile, the case worth reporting.
|
|
348
|
+
#
|
|
349
|
+
# The source is load-bearing, not decoration: Rigor's own repo, every fork of it, and any project vendoring
|
|
350
|
+
# Rigor for development all carry `rigortype` in Gemfile.lock too — under PATH (`remote: .`, from the
|
|
351
|
+
# `gemspec` directive) or GIT. Matching the name alone would fail this check on Rigor itself. Reading the
|
|
352
|
+
# lock by section also keeps a CHECKSUMS entry and a *dependency* line nested under another gem (six-space
|
|
353
|
+
# indent) from counting as a resolution.
|
|
354
|
+
def rubygems_sourced_rigortype?(path)
|
|
355
|
+
section = nil
|
|
356
|
+
in_specs = false
|
|
357
|
+
|
|
358
|
+
File.foreach(path) do |line|
|
|
359
|
+
case line
|
|
360
|
+
when /\A(\S.*?)\s*\z/ then (section = Regexp.last_match(1)) && (in_specs = false)
|
|
361
|
+
when /\A specs:\s*\z/ then in_specs = true
|
|
362
|
+
when /\A rigortype \(/ then return true if section == "GEM" && in_specs
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
false
|
|
366
|
+
rescue StandardError
|
|
367
|
+
false
|
|
368
|
+
end
|
|
369
|
+
|
|
241
370
|
# ------------------------------------------------------------------
|
|
242
371
|
# Reporting
|
|
243
372
|
# ------------------------------------------------------------------
|
|
@@ -272,7 +401,9 @@ module Rigor
|
|
|
272
401
|
if failures.empty? && warnings.empty?
|
|
273
402
|
@out.puts("rigor doctor: all checks passed — no setup problems detected.")
|
|
274
403
|
else
|
|
275
|
-
|
|
404
|
+
# Count warnings too: a warn-only run (a degraded RBS env, a malformed baseline) used to headline
|
|
405
|
+
# "0 issue(s) found" above the very finding it was reporting.
|
|
406
|
+
@out.puts("rigor doctor: #{summary_counts(failures, warnings)}")
|
|
276
407
|
failures.each { |f| print_finding(f) }
|
|
277
408
|
warnings.each { |f| print_finding(f) } unless warnings.empty?
|
|
278
409
|
end
|
|
@@ -280,6 +411,13 @@ module Rigor
|
|
|
280
411
|
passes.each { |f| print_finding(f) } unless passes.empty?
|
|
281
412
|
end
|
|
282
413
|
|
|
414
|
+
def summary_counts(failures, warnings)
|
|
415
|
+
parts = []
|
|
416
|
+
parts << "#{failures.size} issue(s)" unless failures.empty?
|
|
417
|
+
parts << "#{warnings.size} warning(s)" unless warnings.empty?
|
|
418
|
+
"#{parts.join(', ')} found"
|
|
419
|
+
end
|
|
420
|
+
|
|
283
421
|
def print_finding(finding)
|
|
284
422
|
label = case finding[:status]
|
|
285
423
|
when :fail then "[FAIL]"
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "command"
|
|
4
4
|
require_relative "options"
|
|
5
|
+
require_relative "../runtime/jit"
|
|
5
6
|
|
|
6
7
|
require "optionparser"
|
|
7
8
|
|
|
@@ -25,6 +26,10 @@ module Rigor
|
|
|
25
26
|
return CLI::EXIT_USAGE
|
|
26
27
|
end
|
|
27
28
|
|
|
29
|
+
# A long-lived server always outlasts the JIT compile cost, so enable
|
|
30
|
+
# YJIT at boot rather than on the analysis deadline (Runtime::Jit).
|
|
31
|
+
Runtime::Jit.enable_now
|
|
32
|
+
|
|
28
33
|
require_relative "../language_server"
|
|
29
34
|
require_relative "../configuration"
|
|
30
35
|
require "language_server-protocol"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "command"
|
|
4
|
+
require_relative "../runtime/jit"
|
|
4
5
|
|
|
5
6
|
require "optionparser"
|
|
6
7
|
|
|
@@ -27,6 +28,10 @@ module Rigor
|
|
|
27
28
|
return CLI::EXIT_USAGE
|
|
28
29
|
end
|
|
29
30
|
|
|
31
|
+
# A long-lived server always outlasts the JIT compile cost, so enable
|
|
32
|
+
# YJIT at boot rather than on the analysis deadline (Runtime::Jit).
|
|
33
|
+
Runtime::Jit.enable_now
|
|
34
|
+
|
|
30
35
|
require_relative "../mcp"
|
|
31
36
|
require_relative "../version"
|
|
32
37
|
|
|
@@ -7,6 +7,8 @@ require_relative "options"
|
|
|
7
7
|
require_relative "../plugin"
|
|
8
8
|
require_relative "../plugin/loader"
|
|
9
9
|
require_relative "../plugin/services"
|
|
10
|
+
require_relative "../plugin/inflector"
|
|
11
|
+
require_relative "../environment/bundle_sig_discovery"
|
|
10
12
|
require_relative "../reflection"
|
|
11
13
|
require_relative "../type/combinator"
|
|
12
14
|
require_relative "plugins_renderer"
|
|
@@ -67,7 +69,8 @@ module Rigor
|
|
|
67
69
|
configuration = Configuration.load(options.fetch(:config))
|
|
68
70
|
rows = build_rows(configuration)
|
|
69
71
|
|
|
70
|
-
renderer = PluginsRenderer.new(rows: rows, configuration_path: config_path
|
|
72
|
+
renderer = PluginsRenderer.new(rows: rows, configuration_path: config_path,
|
|
73
|
+
inflection: inflection_note(configuration, rows))
|
|
71
74
|
@out.puts(render(renderer, options))
|
|
72
75
|
|
|
73
76
|
any_load_errors = rows.any? { |row| row.fetch(:status) == :load_error }
|
|
@@ -129,6 +132,25 @@ module Rigor
|
|
|
129
132
|
rows + orphan_errors
|
|
130
133
|
end
|
|
131
134
|
|
|
135
|
+
# ADR-90 — activation-time visibility for inflection degradation. When a loaded plugin consumes
|
|
136
|
+
# `Plugin::Inflector` (ADR-39), probe whether the real `ActiveSupport::Inflector` is actually
|
|
137
|
+
# reachable — through Rigor's own gem environment or the analyzed project's bundle — so a standalone
|
|
138
|
+
# install where every inflection-dependent check would silently produce no diagnostics says so here
|
|
139
|
+
# instead of reporting an unqualified `[OK]`. Returns nil when no consumer is loaded (a non-Rails
|
|
140
|
+
# project never sees the probe).
|
|
141
|
+
def inflection_note(configuration, rows)
|
|
142
|
+
loaded_ids = rows.filter_map { |row| row[:id] if row[:status] == :loaded }
|
|
143
|
+
required_by = loaded_ids & Plugin::Inflector::CONSUMER_PLUGIN_IDS
|
|
144
|
+
return nil if required_by.empty?
|
|
145
|
+
|
|
146
|
+
Plugin::Isolation.target_bundle_root ||= Environment::BundleSigDiscovery.resolve_bundle_path(
|
|
147
|
+
bundle_path: configuration.bundler_bundle_path,
|
|
148
|
+
project_root: Dir.pwd,
|
|
149
|
+
auto_detect: configuration.bundler_auto_detect
|
|
150
|
+
)&.to_s
|
|
151
|
+
{ required_by: required_by, available: Plugin::Inflector.available? }
|
|
152
|
+
end
|
|
153
|
+
|
|
132
154
|
def build_services(configuration)
|
|
133
155
|
Plugin::Services.new(
|
|
134
156
|
reflection: Reflection,
|
|
@@ -148,7 +170,10 @@ module Rigor
|
|
|
148
170
|
end
|
|
149
171
|
|
|
150
172
|
if plugin
|
|
151
|
-
|
|
173
|
+
# #194 slice 1 — a loaded plugin's resolved file rides on the registry keyed by gem name (the
|
|
174
|
+
# frozen plugin instance can't carry it). nil when the gem's entry-file basename differs from its
|
|
175
|
+
# name, or the resolver couldn't pin it.
|
|
176
|
+
loaded_row(plugin, gem_name, config, registry.resolved_gem_paths[gem_name])
|
|
152
177
|
else
|
|
153
178
|
# Find the load error whose plugin_ref names this entry (the ref is set by Loader to the gem name on require
|
|
154
179
|
# failures and to the manifest id on later failures).
|
|
@@ -168,9 +193,10 @@ module Rigor
|
|
|
168
193
|
[derived_id, gem_name].include?(plugin.manifest.id)
|
|
169
194
|
end
|
|
170
195
|
|
|
171
|
-
def loaded_row(plugin, gem_name, config)
|
|
196
|
+
def loaded_row(plugin, gem_name, config, resolved_path)
|
|
172
197
|
manifest = plugin.manifest
|
|
173
198
|
identity_fields(gem_name, manifest, config)
|
|
199
|
+
.merge(path: resolved_path)
|
|
174
200
|
.merge(extension_fields(plugin, manifest))
|
|
175
201
|
.merge(narrow_protocol_fields(plugin))
|
|
176
202
|
.merge(load_error: nil)
|
|
@@ -185,7 +211,7 @@ module Rigor
|
|
|
185
211
|
{
|
|
186
212
|
node_rule_types: klass.node_rules.map { |r| r[:node_type].name }.uniq,
|
|
187
213
|
dynamic_return_receivers: klass.dynamic_returns.flat_map { |r| r[:receivers] }.uniq,
|
|
188
|
-
|
|
214
|
+
narrowing_facts_methods: klass.narrowing_facts_rules.flat_map { |r| r[:methods] }.map(&:to_s).uniq
|
|
189
215
|
}
|
|
190
216
|
end
|
|
191
217
|
|
|
@@ -230,6 +256,9 @@ module Rigor
|
|
|
230
256
|
id: entry_id,
|
|
231
257
|
version: nil,
|
|
232
258
|
description: nil,
|
|
259
|
+
# #194 slice 1 — set only when the require SUCCEEDED but a later step failed (the loader stamps the
|
|
260
|
+
# file on the error); a require that failed outright has no resolvable file, so this stays nil.
|
|
261
|
+
path: error&.resolved_path,
|
|
233
262
|
config: config,
|
|
234
263
|
signature_paths: [],
|
|
235
264
|
open_receivers: [],
|
|
@@ -246,7 +275,7 @@ module Rigor
|
|
|
246
275
|
source_rbs_synthesizer: false,
|
|
247
276
|
node_rule_types: [],
|
|
248
277
|
dynamic_return_receivers: [],
|
|
249
|
-
|
|
278
|
+
narrowing_facts_methods: [],
|
|
250
279
|
load_error: error&.message || "plugin did not register or could not be matched to a registered class"
|
|
251
280
|
}
|
|
252
281
|
end
|
|
@@ -310,6 +339,7 @@ module Rigor
|
|
|
310
339
|
id: nil,
|
|
311
340
|
version: nil,
|
|
312
341
|
description: nil,
|
|
342
|
+
path: error.resolved_path,
|
|
313
343
|
config: {},
|
|
314
344
|
signature_paths: [],
|
|
315
345
|
open_receivers: [], owns_receivers: [], produces: [], consumes: [],
|
|
@@ -317,7 +347,7 @@ module Rigor
|
|
|
317
347
|
type_node_resolvers: 0,
|
|
318
348
|
hkt_registrations: 0, hkt_definitions: 0,
|
|
319
349
|
protocol_contracts: 0, source_rbs_synthesizer: false,
|
|
320
|
-
node_rule_types: [], dynamic_return_receivers: [],
|
|
350
|
+
node_rule_types: [], dynamic_return_receivers: [], narrowing_facts_methods: [],
|
|
321
351
|
load_error: error.message
|
|
322
352
|
}
|
|
323
353
|
end
|
|
@@ -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
|