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
|
@@ -14,6 +14,7 @@ require_relative "../inference/parameter_inference_collector"
|
|
|
14
14
|
require_relative "../protection/mutation_scanner"
|
|
15
15
|
require_relative "../protection/test_suite_oracle"
|
|
16
16
|
require_relative "../language_server/project_context"
|
|
17
|
+
require_relative "../runtime/jit"
|
|
17
18
|
require_relative "../scope"
|
|
18
19
|
require_relative "coverage_report"
|
|
19
20
|
require_relative "coverage_renderer"
|
|
@@ -25,6 +26,8 @@ require_relative "mutation_protection_renderer"
|
|
|
25
26
|
require_relative "fused_protection_report"
|
|
26
27
|
require_relative "fused_protection_renderer"
|
|
27
28
|
require_relative "coverage_mutation"
|
|
29
|
+
require_relative "protection_fork_scan"
|
|
30
|
+
require_relative "check_runner_factory"
|
|
28
31
|
require_relative "command"
|
|
29
32
|
|
|
30
33
|
module Rigor
|
|
@@ -51,30 +54,43 @@ module Rigor
|
|
|
51
54
|
DEFAULT_TEST_COMMAND = %w[bundle exec rake].freeze
|
|
52
55
|
|
|
53
56
|
# @return [Integer] CLI exit status.
|
|
54
|
-
def run
|
|
57
|
+
def run # rubocop:disable Metrics/AbcSize
|
|
58
|
+
# Arm deferred YJIT enablement (Runtime::Jit): a scan long enough to
|
|
59
|
+
# amortize JIT compile cost enables mid-flight; a short one finishes
|
|
60
|
+
# first and never pays it. Same seam as `rigor check`.
|
|
61
|
+
Runtime::Jit.enable_after(Runtime::Jit.deadline_seconds)
|
|
55
62
|
options = parse_options
|
|
56
63
|
return mutation_misuse_error if options[:mutation] && !options[:protection]
|
|
57
64
|
return with_tests_misuse_error if options[:with_tests] && !options[:mutation]
|
|
58
65
|
return include_dynamic_misuse_error if options[:include_dynamic] && !options[:with_tests]
|
|
59
66
|
return run_mutation_protection(options) if options[:mutation]
|
|
60
67
|
|
|
61
|
-
|
|
68
|
+
configuration = Configuration.load(options.fetch(:config))
|
|
69
|
+
paths = resolve_paths(configuration)
|
|
62
70
|
return CLI::EXIT_USAGE if paths.nil?
|
|
63
71
|
return usage_error if paths.empty?
|
|
64
72
|
|
|
65
|
-
return run_protection(paths, options) if options[:protection]
|
|
73
|
+
return run_protection(paths, options, configuration) if options[:protection]
|
|
66
74
|
|
|
67
|
-
report = scan_paths(paths,
|
|
75
|
+
report = scan_paths(paths, configuration)
|
|
68
76
|
CoverageRenderer.new(out: @out).render(report, format: options.fetch(:format))
|
|
69
77
|
determine_exit(report, options)
|
|
70
78
|
end
|
|
71
79
|
|
|
72
80
|
private
|
|
73
81
|
|
|
82
|
+
# Like `rigor check`, fall back to the configured `paths:` when no path is given on the command line
|
|
83
|
+
# (`coverage` previously required an explicit path — operational friction the two commands should not
|
|
84
|
+
# differ on). The mutation mode keeps its own git-changed-files default and returns before this.
|
|
85
|
+
def resolve_paths(configuration)
|
|
86
|
+
args = @argv.empty? ? configuration.paths : @argv
|
|
87
|
+
collect_paths(args, command_name: "coverage")
|
|
88
|
+
end
|
|
89
|
+
|
|
74
90
|
def parse_options
|
|
75
91
|
options = { format: "text", threshold: nil, config: nil, protection: false, mutation: false,
|
|
76
92
|
with_tests: false, test_command: DEFAULT_TEST_COMMAND, include_dynamic: false,
|
|
77
|
-
limit: nil, seed: 1 }
|
|
93
|
+
limit: nil, seed: 1, workers: nil }
|
|
78
94
|
OptionParser.new { |opts| define_options(opts, options) }.parse!(@argv)
|
|
79
95
|
options
|
|
80
96
|
end
|
|
@@ -87,6 +103,10 @@ module Rigor
|
|
|
87
103
|
options[:protection] = true
|
|
88
104
|
end
|
|
89
105
|
define_mutation_options(opts, options)
|
|
106
|
+
opts.on("--workers=N", Integer, "With --protection: fork N workers over the scanned files " \
|
|
107
|
+
"(default: config parallel.workers / RIGOR_RACTOR_WORKERS / 0)") do |v|
|
|
108
|
+
options[:workers] = v
|
|
109
|
+
end
|
|
90
110
|
opts.on("--threshold=RATIO", Float, "Exit 1 when the precision (or, with --protection, " \
|
|
91
111
|
"protection/effectiveness) ratio is below RATIO (0.0–1.0)") do |v|
|
|
92
112
|
options[:threshold] = v
|
|
@@ -135,23 +155,39 @@ module Rigor
|
|
|
135
155
|
CLI::EXIT_USAGE
|
|
136
156
|
end
|
|
137
157
|
|
|
138
|
-
def run_protection(paths, options)
|
|
139
|
-
report = scan_protection(paths, options)
|
|
158
|
+
def run_protection(paths, options, configuration)
|
|
159
|
+
report = scan_protection(paths, options, configuration)
|
|
140
160
|
ProtectionRenderer.new(out: @out).render(report, format: options.fetch(:format))
|
|
141
161
|
determine_protection_exit(report, options)
|
|
142
162
|
end
|
|
143
163
|
|
|
144
|
-
|
|
145
|
-
|
|
164
|
+
# Builds the plugin-aware environment and the seeded scope ONCE on the parent, then fork-pools the
|
|
165
|
+
# per-file scan across the resolved worker count (P3-10). The scan is pure-read and each file is
|
|
166
|
+
# independent, so this is a fork-map + ordered reduce: {ProtectionForkScan} returns `{path => result}`
|
|
167
|
+
# and the parent absorbs in `paths` order so the report is byte-identical to a sequential run.
|
|
168
|
+
def scan_protection(paths, options, configuration)
|
|
146
169
|
environment = plugin_aware_environment(configuration)
|
|
147
|
-
|
|
170
|
+
workers = CheckRunnerFactory.resolve_workers(options, configuration)
|
|
171
|
+
scope = scope_with_inferred_params(paths, configuration, environment, workers)
|
|
148
172
|
scanner = Inference::ProtectionScanner.new(scope: scope)
|
|
149
173
|
accumulator = ProtectionAccumulator.new
|
|
150
174
|
|
|
151
|
-
|
|
175
|
+
results = ProtectionForkScan.run(
|
|
176
|
+
paths: paths, scanner: scanner, environment: environment,
|
|
177
|
+
configuration: configuration, workers: workers
|
|
178
|
+
)
|
|
179
|
+
paths.each { |path| absorb_protection_result(accumulator, path, results.fetch(path)) }
|
|
152
180
|
accumulator.to_report
|
|
153
181
|
end
|
|
154
182
|
|
|
183
|
+
def absorb_protection_result(accumulator, path, result)
|
|
184
|
+
if result.is_a?(ProtectionForkScan::ParseError)
|
|
185
|
+
accumulator.record_parse_error_count(path, result.count)
|
|
186
|
+
else
|
|
187
|
+
accumulator.absorb(path, result)
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
155
191
|
# Seed the protection scan's scope with the same cross-file facts `rigor check` resolves against, so a receiver
|
|
156
192
|
# reads the type it actually has rather than a stripped-scope `Dynamic`:
|
|
157
193
|
#
|
|
@@ -168,7 +204,7 @@ module Rigor
|
|
|
168
204
|
#
|
|
169
205
|
# Both span the scanned `paths` only (no whole-project pre-pass) — a site that gains neither is classified exactly
|
|
170
206
|
# as before.
|
|
171
|
-
def scope_with_inferred_params(paths, configuration, environment)
|
|
207
|
+
def scope_with_inferred_params(paths, configuration, environment, workers)
|
|
172
208
|
base = Scope.empty(environment: environment)
|
|
173
209
|
seed = {}
|
|
174
210
|
|
|
@@ -176,7 +212,7 @@ module Rigor
|
|
|
176
212
|
seed[:discovered_classes] = discovered unless discovered.empty?
|
|
177
213
|
|
|
178
214
|
table = Inference::ParameterInferenceCollector.collect(
|
|
179
|
-
files: paths, environment: environment, target_ruby: configuration.target_ruby
|
|
215
|
+
files: paths, environment: environment, target_ruby: configuration.target_ruby, workers: workers
|
|
180
216
|
)
|
|
181
217
|
seed[:param_inferred_types] = table unless table.empty?
|
|
182
218
|
|
|
@@ -200,31 +236,21 @@ module Rigor
|
|
|
200
236
|
CLI::EXIT_USAGE
|
|
201
237
|
end
|
|
202
238
|
|
|
203
|
-
def scan_paths(paths,
|
|
204
|
-
CoverageScan.precision_report(files: paths, configuration:
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
# Delegated to the shared scan module (see {CoverageScan}); the protection path below reuses both, and `rigor
|
|
208
|
-
# check --coverage` reuses `precision_report` over the same machinery.
|
|
209
|
-
def project_environment(configuration)
|
|
210
|
-
CoverageScan.project_environment(configuration)
|
|
239
|
+
def scan_paths(paths, configuration)
|
|
240
|
+
CoverageScan.precision_report(files: paths, configuration: configuration)
|
|
211
241
|
end
|
|
212
242
|
|
|
213
243
|
# The protection scan must see the same receiver types `rigor check` does — including plugin-contributed
|
|
214
244
|
# `dynamic_return` types (a controller's `params` → `ActionController::Parameters`, a `Model.where` →
|
|
215
|
-
# `ActiveRecord::Relation[Model]`). The bare `project_environment` carries only the RBS environment
|
|
216
|
-
# registry), so every plugin-typed receiver reads `Dynamic` and its dispatch site is miscounted as
|
|
217
|
-
# a systematic undercount of what Rigor actually types on a plugin-using project.
|
|
218
|
-
# plugin-aware environment (registry materialised + the per-run prepare pass that
|
|
219
|
-
# controller / model index) exactly as the LSP and the runner do.
|
|
245
|
+
# `ActiveRecord::Relation[Model]`). The bare `CoverageScan.project_environment` carries only the RBS environment
|
|
246
|
+
# (no plugin registry), so every plugin-typed receiver reads `Dynamic` and its dispatch site is miscounted as
|
|
247
|
+
# *unprotected* — a systematic undercount of what Rigor actually types on a plugin-using project.
|
|
248
|
+
# `ProjectContext` builds the plugin-aware environment (registry materialised + the per-run prepare pass that
|
|
249
|
+
# primes producers like the controller / model index) exactly as the LSP and the runner do.
|
|
220
250
|
def plugin_aware_environment(configuration)
|
|
221
251
|
LanguageServer::ProjectContext.new(configuration: configuration).environment
|
|
222
252
|
end
|
|
223
253
|
|
|
224
|
-
def scan_one(path, scanner, accumulator, configuration)
|
|
225
|
-
CoverageScan.scan_into(path, scanner, accumulator, configuration)
|
|
226
|
-
end
|
|
227
|
-
|
|
228
254
|
def determine_exit(report, options)
|
|
229
255
|
return 1 unless report.parse_errors.empty?
|
|
230
256
|
|
|
@@ -33,7 +33,9 @@ module Rigor
|
|
|
33
33
|
FORMATS.include?(format)
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
-
# Renders `result` in the named CI format. Callers gate on {.supports?} first
|
|
36
|
+
# Renders `result` in the named CI format. Callers gate on {.supports?} first, so an unrecognised format means
|
|
37
|
+
# {FORMATS} and the dispatch below have drifted apart — raise rather than return nil, which every caller would
|
|
38
|
+
# then have to guard.
|
|
37
39
|
def render(result, format)
|
|
38
40
|
case format
|
|
39
41
|
when "sarif" then Sarif.new(result).render
|
|
@@ -42,6 +44,7 @@ module Rigor
|
|
|
42
44
|
when "checkstyle" then Checkstyle.new(result).render
|
|
43
45
|
when "junit" then Junit.new(result).render
|
|
44
46
|
when "teamcity" then Teamcity.new(result).render
|
|
47
|
+
else raise ArgumentError, "unsupported format: #{format}"
|
|
45
48
|
end
|
|
46
49
|
end
|
|
47
50
|
|
|
@@ -20,9 +20,6 @@ module Rigor
|
|
|
20
20
|
#
|
|
21
21
|
# `<name>` resolves a category-qualified path (`handbook/03-narrowing`), a prefixed basename (`03-narrowing`), or a
|
|
22
22
|
# short name (`narrowing`, when it is unique across categories).
|
|
23
|
-
#
|
|
24
|
-
# The pre-v0.3.0 verb spellings `rigor docs list` / `rigor docs path <name>` still work but emit a stderr
|
|
25
|
-
# deprecation notice; they are removed in v0.3.0 (see docs/ROADMAP.md § "Scheduled CLI deprecations").
|
|
26
23
|
class DocsCommand < Command
|
|
27
24
|
USAGE = <<~USAGE
|
|
28
25
|
Usage: rigor docs [<name>] [--path <name>] [--list [<category>]]
|
|
@@ -46,10 +43,6 @@ module Rigor
|
|
|
46
43
|
rigor docs editor-integration
|
|
47
44
|
rigor docs --path 17-driving-improvement
|
|
48
45
|
rigor docs --list handbook
|
|
49
|
-
|
|
50
|
-
Deprecated (removed in v0.3.0) — use the flags above:
|
|
51
|
-
rigor docs list -> rigor docs --list
|
|
52
|
-
rigor docs path <name> -> rigor docs --path <name>
|
|
53
46
|
USAGE
|
|
54
47
|
|
|
55
48
|
# The bundled docs live at `<gem_root>/docs/`. From `lib/rigor/cli/docs_command.rb` that is three directories up.
|
|
@@ -58,18 +51,8 @@ module Rigor
|
|
|
58
51
|
HANDBOOK_ROOT = File.join(DOCS_ROOT, "handbook")
|
|
59
52
|
LLMS_INDEX = File.join(DOCS_ROOT, "llms.txt")
|
|
60
53
|
|
|
61
|
-
# The verb subcommands the flags superseded keep working with a stderr deprecation notice until this version drops
|
|
62
|
-
# them. Each maps to the canonical advice printed and the flag it rewrites to.
|
|
63
|
-
LEGACY_VERB_REMOVAL = "v0.3.0"
|
|
64
|
-
LEGACY_VERBS = {
|
|
65
|
-
"list" => { old: "list", advice: "--list", flag: "--list" },
|
|
66
|
-
"path" => { old: "path <name>", advice: "--path <name>", flag: "--path" }
|
|
67
|
-
}.freeze
|
|
68
|
-
|
|
69
54
|
# @return [Integer] CLI exit status.
|
|
70
55
|
def run
|
|
71
|
-
rewrite_legacy_verb!
|
|
72
|
-
|
|
73
56
|
case @argv.first
|
|
74
57
|
when nil
|
|
75
58
|
run_index
|
|
@@ -92,19 +75,6 @@ module Rigor
|
|
|
92
75
|
|
|
93
76
|
private
|
|
94
77
|
|
|
95
|
-
# Translate a deprecated verb spelling into its flag form, warning once on stderr, so the dispatch above only
|
|
96
|
-
# handles canonical forms.
|
|
97
|
-
def rewrite_legacy_verb!
|
|
98
|
-
spec = LEGACY_VERBS[@argv.first]
|
|
99
|
-
return unless spec
|
|
100
|
-
|
|
101
|
-
@err.puts(
|
|
102
|
-
"rigor docs: `#{spec.fetch(:old)}` is deprecated and will be removed in " \
|
|
103
|
-
"#{LEGACY_VERB_REMOVAL}; use `rigor docs #{spec.fetch(:advice)}` instead."
|
|
104
|
-
)
|
|
105
|
-
@argv[0] = spec.fetch(:flag)
|
|
106
|
-
end
|
|
107
|
-
|
|
108
78
|
def run_index
|
|
109
79
|
if File.file?(LLMS_INDEX)
|
|
110
80
|
@out.write(File.read(LLMS_INDEX))
|
|
@@ -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[
|
|
@@ -63,8 +65,9 @@ module Rigor
|
|
|
63
65
|
# 3. RBS environment check.
|
|
64
66
|
findings.concat(check_rbs_environment(result))
|
|
65
67
|
|
|
66
|
-
# 4. Plugin load check.
|
|
67
|
-
|
|
68
|
+
# 4. Plugin load check + 8. installation-skew check share one loaded registry.
|
|
69
|
+
registry = load_plugin_registry(configuration)
|
|
70
|
+
findings.concat(check_plugins(registry))
|
|
68
71
|
|
|
69
72
|
# 5. Baseline drift check.
|
|
70
73
|
findings.concat(check_baseline(configuration, result))
|
|
@@ -72,6 +75,12 @@ module Rigor
|
|
|
72
75
|
# 6. Rails locked but no Rails plugins.
|
|
73
76
|
findings.concat(check_rails_plugins(configuration))
|
|
74
77
|
|
|
78
|
+
# 7. Rigor itself resolved as a project dependency.
|
|
79
|
+
findings.concat(check_gemfile_install)
|
|
80
|
+
|
|
81
|
+
# 8. A bundled plugin loaded from a different rigortype installation than the engine (#194 slice 3).
|
|
82
|
+
findings.concat(check_plugin_skew(registry))
|
|
83
|
+
|
|
75
84
|
report(findings, options.fetch(:format))
|
|
76
85
|
findings.any? { |f| f[:status] == :fail } ? 1 : 0
|
|
77
86
|
rescue OptionParser::InvalidArgument => e
|
|
@@ -122,37 +131,67 @@ module Rigor
|
|
|
122
131
|
stats = result.stats
|
|
123
132
|
return [] unless stats
|
|
124
133
|
|
|
134
|
+
quarantined = quarantined_signature_diagnostics(result)
|
|
125
135
|
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
|
-
]
|
|
136
|
+
[empty_rbs_env_finding(stats)]
|
|
137
|
+
elsif quarantined.any?
|
|
138
|
+
[degraded_rbs_env_finding(stats, quarantined)]
|
|
136
139
|
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
|
-
]
|
|
140
|
+
[healthy_rbs_env_finding(stats)]
|
|
145
141
|
end
|
|
146
142
|
end
|
|
147
143
|
|
|
148
|
-
def
|
|
144
|
+
def empty_rbs_env_finding(stats)
|
|
145
|
+
{
|
|
146
|
+
check: CHECK_RBS,
|
|
147
|
+
status: :fail,
|
|
148
|
+
message: "RBS environment is empty (#{stats.rbs_classes_total} classes available)",
|
|
149
|
+
hint: "The RBS environment failed to build or loaded no signatures. " \
|
|
150
|
+
"Check `signature_paths:` for duplicate declarations, or run " \
|
|
151
|
+
"`rbs collection install` for gem signatures."
|
|
152
|
+
}
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# The env is non-empty precisely BECAUSE the broken file was quarantined, so the class count alone reads
|
|
156
|
+
# as healthy — this is the case doctor used to pass. Classify the diagnostic the run already produced
|
|
157
|
+
# (ADR-77: route existing evidence) rather than re-probing the sig tree.
|
|
158
|
+
def degraded_rbs_env_finding(stats, quarantined)
|
|
159
|
+
{
|
|
160
|
+
check: CHECK_RBS,
|
|
161
|
+
status: :warn,
|
|
162
|
+
message: "RBS environment degraded — #{quarantined.size} `signature_paths:` file(s) " \
|
|
163
|
+
"skipped, #{stats.rbs_classes_total} classes available",
|
|
164
|
+
hint: "An unparseable `.rbs` is skipped so the rest of the env survives, which makes the " \
|
|
165
|
+
"run quieter, not cleaner: the types it declares are gone. Run `rbs validate` on your " \
|
|
166
|
+
"`sig/` set and fix the parse error. To make this fail the build, opt into the " \
|
|
167
|
+
"`reject-unparseable-signatures` bleeding-edge feature."
|
|
168
|
+
}
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def healthy_rbs_env_finding(stats)
|
|
172
|
+
{
|
|
173
|
+
check: CHECK_RBS,
|
|
174
|
+
status: :pass,
|
|
175
|
+
message: "RBS environment healthy (#{stats.rbs_classes_total} classes available)",
|
|
176
|
+
hint: nil
|
|
177
|
+
}
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def quarantined_signature_diagnostics(result)
|
|
181
|
+
result.diagnostics.select { |diagnostic| diagnostic.rule == "rbs.coverage.quarantined-signature" }
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def load_plugin_registry(configuration)
|
|
149
185
|
services = Plugin::Services.new(
|
|
150
186
|
reflection: Reflection,
|
|
151
187
|
type: Type::Combinator,
|
|
152
188
|
configuration: configuration,
|
|
153
189
|
cache_store: nil
|
|
154
190
|
)
|
|
155
|
-
|
|
191
|
+
Plugin::Loader.load(configuration: configuration, services: services)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def check_plugins(registry)
|
|
156
195
|
errors = registry.load_errors
|
|
157
196
|
return [] if errors.empty?
|
|
158
197
|
|
|
@@ -166,6 +205,52 @@ module Rigor
|
|
|
166
205
|
]
|
|
167
206
|
end
|
|
168
207
|
|
|
208
|
+
# WD5 (#194 slice 3) — a bundled plugin should load from the engine's own `plugins/` tree; slice 2
|
|
209
|
+
# anchors it there. This guards the residual anchoring cannot see: a bundled plugin whose resolved file
|
|
210
|
+
# sits OUTSIDE the engine tree — the fallback-require path, or a genuinely mixed installation where a
|
|
211
|
+
# foreign copy was already loaded. That is the #194 skew, which ran the engine with a load-bearing
|
|
212
|
+
# false-positive gate silently missing. Only plugins the engine bundles are checked; a third-party /
|
|
213
|
+
# project-bundle plugin is not the engine's to vouch for and is never flagged, and a nil resolved path
|
|
214
|
+
# (an injected requirer, an in-process no-op load) carries no provenance and is skipped.
|
|
215
|
+
def check_plugin_skew(registry)
|
|
216
|
+
registry.resolved_gem_paths.filter_map do |gem_name, resolved_path|
|
|
217
|
+
next if resolved_path.nil?
|
|
218
|
+
next unless Plugin::Loader.bundled_plugin_path(gem_name)
|
|
219
|
+
next if inside_engine_tree?(resolved_path)
|
|
220
|
+
|
|
221
|
+
plugin_skew_finding(gem_name, resolved_path)
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def plugin_skew_finding(gem_name, resolved_path)
|
|
226
|
+
{
|
|
227
|
+
check: CHECK_PLUGIN_SKEW,
|
|
228
|
+
status: :warn,
|
|
229
|
+
message: "Plugin #{gem_name} loaded from a different rigortype installation than the engine " \
|
|
230
|
+
"(loaded from #{resolved_path}; engine at #{Plugin::Loader::ENGINE_ROOT})",
|
|
231
|
+
hint: "The engine and its bundled plugins are versioned together, so a copy resolved from another " \
|
|
232
|
+
"installation can run the engine with a mismatched plugin (the cause of wrong diagnostics in " \
|
|
233
|
+
"#194). Ensure a single `rigortype` is on the load path — remove any separately-installed " \
|
|
234
|
+
"`rigortype` gem that shadows this checkout, or reinstall so the engine and its `plugins/` " \
|
|
235
|
+
"tree come from one place."
|
|
236
|
+
}
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# True when `resolved_path` lives inside the engine's own tree. Both sides are resolved through
|
|
240
|
+
# `File.realpath` first so a symlinked gem home / checkout (e.g. macOS `/var` → `/private/var`) does not
|
|
241
|
+
# read as foreign; a vanished file degrades to `File.expand_path` rather than raising.
|
|
242
|
+
def inside_engine_tree?(resolved_path)
|
|
243
|
+
engine = real_or_expanded(Plugin::Loader::ENGINE_ROOT)
|
|
244
|
+
target = real_or_expanded(resolved_path)
|
|
245
|
+
target == engine || target.start_with?("#{engine}#{File::SEPARATOR}")
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def real_or_expanded(path)
|
|
249
|
+
File.realpath(path)
|
|
250
|
+
rescue StandardError
|
|
251
|
+
File.expand_path(path)
|
|
252
|
+
end
|
|
253
|
+
|
|
169
254
|
def check_baseline(configuration, result)
|
|
170
255
|
path = configuration.baseline_path
|
|
171
256
|
return [] if path.nil? || !File.exist?(path)
|
|
@@ -238,6 +323,52 @@ module Rigor
|
|
|
238
323
|
false
|
|
239
324
|
end
|
|
240
325
|
|
|
326
|
+
# Rigor is a tool, not a library (ADR-27): resolving it as one of the project's own dependencies pins the
|
|
327
|
+
# application to Rigor's Ruby and drags Rigor's dependency graph into the app's resolution. The gem carries
|
|
328
|
+
# guardrails against arriving here, but they only fire during install and at boot — a project that already
|
|
329
|
+
# made the mistake never sees them again, so doctor names the state.
|
|
330
|
+
def check_gemfile_install
|
|
331
|
+
lock = File.join(Dir.pwd, "Gemfile.lock")
|
|
332
|
+
return [] unless File.file?(lock)
|
|
333
|
+
return [] unless rubygems_sourced_rigortype?(lock)
|
|
334
|
+
|
|
335
|
+
[
|
|
336
|
+
{
|
|
337
|
+
check: CHECK_GEMFILE,
|
|
338
|
+
status: :fail,
|
|
339
|
+
message: "Rigor is resolved as a dependency of this project (`rigortype` under GEM in Gemfile.lock)",
|
|
340
|
+
hint: "Remove `rigortype` from the Gemfile and install Rigor on its own — see " \
|
|
341
|
+
"https://github.com/rigortype/rigor/blob/master/docs/install.md. To keep the version " \
|
|
342
|
+
"pinned, use `mise use --pin gem:rigortype`, or an isolated `BUNDLE_GEMFILE` holding only " \
|
|
343
|
+
"Rigor (the CI chapter's pattern), which leaves your application's resolution untouched."
|
|
344
|
+
}
|
|
345
|
+
]
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# True only when `rigortype` resolves from a GEM remote — i.e. an ordinary `gem "rigortype"` in the app's
|
|
349
|
+
# Gemfile, the case worth reporting.
|
|
350
|
+
#
|
|
351
|
+
# The source is load-bearing, not decoration: Rigor's own repo, every fork of it, and any project vendoring
|
|
352
|
+
# Rigor for development all carry `rigortype` in Gemfile.lock too — under PATH (`remote: .`, from the
|
|
353
|
+
# `gemspec` directive) or GIT. Matching the name alone would fail this check on Rigor itself. Reading the
|
|
354
|
+
# lock by section also keeps a CHECKSUMS entry and a *dependency* line nested under another gem (six-space
|
|
355
|
+
# indent) from counting as a resolution.
|
|
356
|
+
def rubygems_sourced_rigortype?(path)
|
|
357
|
+
section = nil
|
|
358
|
+
in_specs = false
|
|
359
|
+
|
|
360
|
+
File.foreach(path) do |line|
|
|
361
|
+
case line
|
|
362
|
+
when /\A(\S.*?)\s*\z/ then (section = Regexp.last_match(1)) && (in_specs = false)
|
|
363
|
+
when /\A specs:\s*\z/ then in_specs = true
|
|
364
|
+
when /\A rigortype \(/ then return true if section == "GEM" && in_specs
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
false
|
|
368
|
+
rescue StandardError
|
|
369
|
+
false
|
|
370
|
+
end
|
|
371
|
+
|
|
241
372
|
# ------------------------------------------------------------------
|
|
242
373
|
# Reporting
|
|
243
374
|
# ------------------------------------------------------------------
|
|
@@ -272,7 +403,9 @@ module Rigor
|
|
|
272
403
|
if failures.empty? && warnings.empty?
|
|
273
404
|
@out.puts("rigor doctor: all checks passed — no setup problems detected.")
|
|
274
405
|
else
|
|
275
|
-
|
|
406
|
+
# Count warnings too: a warn-only run (a degraded RBS env, a malformed baseline) used to headline
|
|
407
|
+
# "0 issue(s) found" above the very finding it was reporting.
|
|
408
|
+
@out.puts("rigor doctor: #{summary_counts(failures, warnings)}")
|
|
276
409
|
failures.each { |f| print_finding(f) }
|
|
277
410
|
warnings.each { |f| print_finding(f) } unless warnings.empty?
|
|
278
411
|
end
|
|
@@ -280,6 +413,13 @@ module Rigor
|
|
|
280
413
|
passes.each { |f| print_finding(f) } unless passes.empty?
|
|
281
414
|
end
|
|
282
415
|
|
|
416
|
+
def summary_counts(failures, warnings)
|
|
417
|
+
parts = []
|
|
418
|
+
parts << "#{failures.size} issue(s)" unless failures.empty?
|
|
419
|
+
parts << "#{warnings.size} warning(s)" unless warnings.empty?
|
|
420
|
+
"#{parts.join(', ')} found"
|
|
421
|
+
end
|
|
422
|
+
|
|
283
423
|
def print_finding(finding)
|
|
284
424
|
label = case finding[:status]
|
|
285
425
|
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
|