rigortype 0.3.5 → 0.3.7
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 +5 -5
- data/data/builtins/ruby_core/date.yml +6 -3
- data/data/builtins/ruby_core/enumerable.yml +1 -0
- data/data/builtins/ruby_core/exception.yml +2 -1
- data/data/builtins/ruby_core/file.yml +28 -14
- data/data/builtins/ruby_core/hash.yml +5 -2
- data/data/builtins/ruby_core/io.yml +33 -16
- data/data/builtins/ruby_core/random.yml +2 -1
- data/data/builtins/ruby_core/re.yml +6 -3
- data/data/builtins/ruby_core/struct.yml +2 -0
- data/data/builtins/ruby_core/time.yml +2 -1
- data/data/gem_overlay/activesupport/core_ext.rbs +442 -11
- data/docs/handbook/02-everyday-types.md +1 -1
- data/docs/handbook/04-tuples-and-shapes.md +1 -1
- data/docs/handbook/08-understanding-errors.md +1 -1
- data/docs/handbook/09-plugins.md +2 -2
- data/docs/handbook/10-sorbet.md +1 -1
- data/docs/handbook/README.md +2 -2
- data/docs/handbook/appendix-go.md +1 -1
- data/docs/handbook/appendix-java-csharp.md +2 -2
- data/docs/handbook/appendix-mypy.md +1 -1
- data/docs/handbook/appendix-protocols-and-structural-typing.md +2 -2
- data/docs/handbook/appendix-rust.md +1 -1
- data/docs/handbook/appendix-type-theory.md +2 -2
- data/docs/handbook/appendix-typescript.md +4 -4
- data/docs/manual/02-cli-reference.md +29 -16
- data/docs/manual/03-configuration.md +1 -1
- data/docs/manual/04-diagnostics.md +12 -0
- data/docs/manual/05-inspecting-types.md +20 -8
- data/docs/manual/07-plugins.md +4 -0
- data/docs/manual/08-skills.md +1 -1
- data/docs/manual/11-ci.md +1 -1
- data/docs/manual/12-caching.md +1 -1
- data/docs/manual/15-type-protection-coverage.md +1 -1
- data/docs/manual/18-removing-dead-code.md +13 -4
- data/docs/manual/19-effect-labels.md +19 -12
- data/docs/manual/README.md +2 -2
- data/docs/manual/plugins/rigor-actionmailer.md +4 -4
- data/docs/manual/plugins/rigor-actionpack.md +61 -0
- data/docs/manual/plugins/rigor-activejob.md +3 -3
- data/docs/manual/plugins/rigor-activerecord.md +78 -5
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +114 -12
- data/docs/manual/plugins/rigor-rails-i18n.md +5 -5
- data/docs/manual/plugins/rigor-rspec.md +6 -2
- data/docs/manual/plugins/rigor-sidekiq.md +22 -0
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +12 -0
- data/lib/rigor/analysis/check_rules/dead_version_guard_arms.rb +98 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -3
- data/lib/rigor/analysis/check_rules/published_constant_guard.rb +199 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +1 -2
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -2
- data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +6 -12
- data/lib/rigor/analysis/check_rules.rb +383 -48
- data/lib/rigor/analysis/crash_signature.rb +116 -0
- data/lib/rigor/analysis/dependency_recorder.rb +18 -3
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/diagnostic.rb +17 -6
- data/lib/rigor/analysis/effects_cache_probe.rb +132 -0
- data/lib/rigor/analysis/incremental.rb +28 -0
- data/lib/rigor/analysis/incremental_session.rb +44 -9
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +1 -2
- data/lib/rigor/analysis/reachability/graph.rb +44 -16
- data/lib/rigor/analysis/reachability/plugin_roots.rb +11 -8
- data/lib/rigor/analysis/reachability/scan.rb +27 -7
- data/lib/rigor/analysis/reachability/scan_cache.rb +130 -0
- data/lib/rigor/analysis/result.rb +32 -0
- data/lib/rigor/analysis/run_cache_key.rb +53 -1
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +93 -1
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +14 -8
- data/lib/rigor/analysis/runner/pool_coordinator.rb +34 -0
- data/lib/rigor/analysis/runner/project_pre_passes.rb +15 -11
- data/lib/rigor/analysis/runner/run_snapshots.rb +7 -2
- data/lib/rigor/analysis/runner.rb +298 -38
- data/lib/rigor/analysis/worker_session.rb +10 -1
- data/lib/rigor/bleeding_edge.rb +7 -6
- data/lib/rigor/builtins/hkt_builtins.rb +45 -6
- data/lib/rigor/builtins/predefined_constant_refinements.rb +108 -59
- data/lib/rigor/builtins/static_return_refinements.rb +20 -1
- data/lib/rigor/cache/descriptor.rb +42 -2
- data/lib/rigor/cache/file_digest.rb +20 -2
- data/lib/rigor/cache/incremental_snapshot.rb +40 -3
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +31 -8
- data/lib/rigor/cache/store.rb +10 -1
- data/lib/rigor/cli/check_command.rb +67 -48
- data/lib/rigor/cli/coverage_command.rb +28 -24
- data/lib/rigor/cli/coverage_mutation.rb +13 -17
- data/lib/rigor/cli/coverage_scan.rb +47 -6
- data/lib/rigor/cli/doc_links.rb +100 -0
- data/lib/rigor/cli/docs_command.rb +32 -2
- data/lib/rigor/cli/effects_command.rb +27 -2
- data/lib/rigor/cli/effects_diff_renderer.rb +82 -12
- data/lib/rigor/cli/effects_explain_renderer.rb +6 -3
- data/lib/rigor/cli/effects_snapshot_command.rb +52 -7
- data/lib/rigor/cli/fused_protection_renderer.rb +19 -2
- data/lib/rigor/cli/fused_protection_report.rb +23 -2
- data/lib/rigor/cli/measurement_integrity_warning.rb +57 -0
- data/lib/rigor/cli/mutation_protection_renderer.rb +22 -2
- data/lib/rigor/cli/mutation_protection_report.rb +31 -2
- data/lib/rigor/cli/sig_gen_command.rb +18 -0
- data/lib/rigor/cli/type_of_command.rb +188 -41
- data/lib/rigor/cli/type_of_renderer.rb +72 -3
- data/lib/rigor/cli/type_scan_command.rb +10 -1
- data/lib/rigor/cli/unused_command.rb +65 -16
- data/lib/rigor/cli.rb +22 -9
- data/lib/rigor/configuration.rb +7 -4
- data/lib/rigor/effects/definition_lines.rb +100 -0
- data/lib/rigor/effects/envelope_check.rb +18 -1
- data/lib/rigor/effects/liskov_check.rb +17 -8
- data/lib/rigor/effects/plugin_facts.rb +1 -1
- data/lib/rigor/effects/scanner.rb +2 -3
- data/lib/rigor/effects/signature_sources.rb +13 -2
- data/lib/rigor/effects/snapshot.rb +53 -21
- data/lib/rigor/effects/snapshot_diff.rb +26 -3
- data/lib/rigor/environment/bundle_sig_discovery.rb +64 -13
- data/lib/rigor/environment/lockfile_resolver.rb +15 -5
- data/lib/rigor/environment/missing_gem_constant_index.rb +45 -4
- data/lib/rigor/environment/rbs_hierarchy.rb +16 -22
- data/lib/rigor/environment/rbs_loader.rb +525 -46
- data/lib/rigor/environment.rb +38 -15
- data/lib/rigor/inference/acceptance.rb +172 -20
- data/lib/rigor/inference/captured_locals.rb +62 -0
- data/lib/rigor/inference/content_join.rb +347 -0
- data/lib/rigor/inference/def_handle.rb +15 -3
- data/lib/rigor/inference/def_node_resolver.rb +62 -3
- data/lib/rigor/inference/expression_typer.rb +1020 -141
- data/lib/rigor/inference/fork_map.rb +6 -1
- data/lib/rigor/inference/hkt_reducer.rb +12 -1
- data/lib/rigor/inference/hkt_registry.rb +46 -0
- data/lib/rigor/inference/hkt_sugar_translator.rb +93 -0
- data/lib/rigor/inference/index_write_widening.rb +48 -0
- data/lib/rigor/inference/indexed_narrowing.rb +14 -0
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +7 -5
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +10 -0
- data/lib/rigor/inference/method_dispatcher/json_folding.rb +58 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +81 -38
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +54 -17
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +1 -2
- data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +63 -0
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +63 -10
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +104 -0
- data/lib/rigor/inference/method_dispatcher/universal_object_dispatch.rb +81 -0
- data/lib/rigor/inference/method_dispatcher.rb +155 -14
- data/lib/rigor/inference/method_parameter_binder.rb +3 -3
- data/lib/rigor/inference/mutation_widening.rb +244 -174
- data/lib/rigor/inference/narrowing.rb +331 -27
- data/lib/rigor/inference/parameter_inference_collector.rb +1 -0
- data/lib/rigor/inference/pre_eval_constants.rb +15 -0
- data/lib/rigor/inference/precision_scanner.rb +5 -2
- data/lib/rigor/inference/project_patched_scanner.rb +2 -1
- data/lib/rigor/inference/rbs_type_translator.rb +89 -39
- data/lib/rigor/inference/scope_indexer.rb +1493 -140
- data/lib/rigor/inference/statement_evaluator.rb +276 -91
- data/lib/rigor/inference/struct_fold_safety.rb +186 -27
- data/lib/rigor/inference/synthetic_method_scanner.rb +7 -0
- data/lib/rigor/inference/version_guard.rb +229 -0
- data/lib/rigor/language_server/selection_range_provider.rb +1 -1
- data/lib/rigor/plugin/base.rb +3 -2
- data/lib/rigor/plugin/inflector.rb +14 -5
- data/lib/rigor/plugin/io_boundary.rb +107 -4
- data/lib/rigor/plugin/loader.rb +14 -0
- data/lib/rigor/protection/analysis_guard.rb +60 -0
- data/lib/rigor/protection/closure_kill_oracle.rb +43 -38
- data/lib/rigor/protection/diagnostic_oracle.rb +12 -4
- data/lib/rigor/protection/discovery_seed.rb +1 -1
- data/lib/rigor/protection/measurement_integrity.rb +36 -0
- data/lib/rigor/protection/mutation_scanner.rb +61 -6
- data/lib/rigor/protection/mutator.rb +121 -18
- data/lib/rigor/rbs_extended/envelope_scanner.rb +9 -0
- data/lib/rigor/reflection.rb +182 -30
- data/lib/rigor/scope/discovery_index.rb +39 -1
- data/lib/rigor/scope.rb +385 -4
- data/lib/rigor/sig_gen/classification.rb +12 -1
- data/lib/rigor/sig_gen/generator.rb +254 -5
- data/lib/rigor/sig_gen/renderer.rb +1 -6
- data/lib/rigor/sig_gen/writer.rb +3 -0
- data/lib/rigor/source/constant_path.rb +79 -2
- data/lib/rigor/triage/catalogue.rb +1 -1
- data/lib/rigor/type/combinator.rb +10 -0
- data/lib/rigor/type/maybe.rb +47 -0
- data/lib/rigor/type/refined.rb +1 -2
- data/lib/rigor/type/result.rb +53 -0
- data/lib/rigor/type.rb +2 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +3 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +58 -8
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +15 -2
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +4 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +3 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +39 -14
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +36 -3
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -2
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +21 -5
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +47 -8
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +13 -3
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +242 -4
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +3 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +7 -3
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +62 -11
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +15 -2
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +3 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +5 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +798 -22
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +156 -13
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +140 -40
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +2 -2
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +20 -6
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +14 -4
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +6 -3
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +23 -6
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +264 -9
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +876 -23
- data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +99 -0
- data/plugins/rigor-dry-monads/lib/rigor-dry-monads.rb +5 -0
- data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +34 -0
- data/plugins/rigor-ethon/lib/rigor-ethon.rb +3 -0
- data/plugins/rigor-ethon/sig/ethon.rbs +27 -0
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -2
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +198 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/binding_recognizer.rb +75 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/catalog.rb +64 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +173 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +46 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +161 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +121 -0
- data/plugins/rigor-ffi/lib/rigor-ffi.rb +3 -0
- data/plugins/rigor-ffi/sig/ffi.rbs +67 -0
- data/plugins/rigor-ffi-rzmq/lib/rigor/plugin/ffi_rzmq.rb +19 -0
- data/plugins/rigor-ffi-rzmq/lib/rigor-ffi-rzmq.rb +3 -0
- data/plugins/rigor-ffi-rzmq/sig/ffi_rzmq.rbs +29 -0
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -4
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +2 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +2 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +2 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +2 -1
- data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +156 -11
- data/plugins/rigor-rbnacl/lib/rigor/plugin/rbnacl.rb +30 -0
- data/plugins/rigor-rbnacl/lib/rigor-rbnacl.rb +3 -0
- data/plugins/rigor-rbnacl/sig/rbnacl.rbs +22 -0
- data/plugins/rigor-sassc/lib/rigor/plugin/sassc.rb +55 -0
- data/plugins/rigor-sassc/lib/rigor-sassc.rb +3 -0
- data/plugins/rigor-sassc/sig/sassc.rbs +24 -0
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +32 -11
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +3 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +4 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +63 -10
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +15 -2
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +61 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +2 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +4 -2
- data/sig/rigor/analysis/baseline.rbs +1 -1
- data/sig/rigor/environment.rbs +3 -0
- data/sig/rigor/inference.rbs +4 -1
- data/sig/rigor/plugin/io_boundary.rbs +2 -0
- data/sig/rigor/reflection.rbs +3 -1
- data/sig/rigor/scope.rbs +25 -1
- data/sig/rigor/type.rbs +95 -1
- data/sig/rigor.rbs +16 -0
- data/skills/rigor-ci-setup/SKILL.md +2 -2
- data/skills/rigor-editor-setup/SKILL.md +2 -2
- data/skills/rigor-mcp-setup/SKILL.md +2 -2
- data/skills/rigor-monkeypatch-resolve/SKILL.md +1 -1
- data/skills/rigor-plugin-review/SKILL.md +3 -3
- metadata +73 -3
|
@@ -11,7 +11,6 @@ require_relative "../analysis/rule_catalog"
|
|
|
11
11
|
# The baseline filter runs on EVERY exit path — including the ADR-87 WD4 cache-HIT fast path, which skips
|
|
12
12
|
# `load_check_dependencies` — so it must be a load-time require. It pulls only YAML, never the engine.
|
|
13
13
|
require_relative "../analysis/baseline"
|
|
14
|
-
require_relative "../runtime/jit"
|
|
15
14
|
require_relative "command"
|
|
16
15
|
require_relative "options"
|
|
17
16
|
require_relative "diagnostic_formats"
|
|
@@ -34,12 +33,10 @@ module Rigor
|
|
|
34
33
|
# concerns that are clearer read together than split across micro-classes.
|
|
35
34
|
class CheckCommand < Command # rubocop:disable Metrics/ClassLength
|
|
36
35
|
# @return [Integer] CLI exit status.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
# long run JITs its dominant tail (Runtime::Jit).
|
|
42
|
-
Runtime::Jit.enable_after(Runtime::Jit.deadline_seconds)
|
|
36
|
+
#
|
|
37
|
+
# Deferred YJIT enablement (Runtime::Jit) is armed by `CLI#dispatch` for every command, this
|
|
38
|
+
# one included, before any analysis work runs.
|
|
39
|
+
def run # rubocop:disable Metrics/AbcSize
|
|
43
40
|
# ADR-87 WD4 — parse options + resolve config WITHOUT the inference engine, so the run-cache hit probe
|
|
44
41
|
# can run first. The heavy engine (`load_check_dependencies`) loads only on a miss / non-cacheable run.
|
|
45
42
|
options = parse_check_options
|
|
@@ -50,7 +47,7 @@ module Rigor
|
|
|
50
47
|
configuration = apply_bleeding_edge_override(configuration, options)
|
|
51
48
|
config_warnings = warn_unresolved_config(configuration)
|
|
52
49
|
cache_root = configuration.cache_path
|
|
53
|
-
handle_clear_cache(cache_root) if options.fetch(:clear_cache)
|
|
50
|
+
handle_clear_cache(cache_root, options.fetch(:format)) if options.fetch(:clear_cache)
|
|
54
51
|
|
|
55
52
|
# ADR-87 WD4 — try to serve the whole run from the ADR-45 cache before booting the engine. A hit boots
|
|
56
53
|
# only CLI + config + cache + digest code (no `rigor/inference`), skipping the plugin prepass + env
|
|
@@ -76,7 +73,7 @@ module Rigor
|
|
|
76
73
|
write_run_stats(result.stats) if result.stats
|
|
77
74
|
write_trace_appendices
|
|
78
75
|
runner.cache_store&.evict!
|
|
79
|
-
write_cache_stats(cache_root, runner.cache_store) if options.fetch(:cache_stats)
|
|
76
|
+
write_cache_stats(cache_root, runner.cache_store, options.fetch(:format)) if options.fetch(:cache_stats)
|
|
80
77
|
|
|
81
78
|
exit_code = result.success? ? 0 : 1
|
|
82
79
|
exit_code = 1 if baseline_strict_violation?(raw_result.diagnostics, configuration, options)
|
|
@@ -195,7 +192,8 @@ module Rigor
|
|
|
195
192
|
incremental = normalize_diagnostics(session.reanalyze_subset(subset))
|
|
196
193
|
full = normalize_diagnostics(verify_full_diagnostics(configuration, paths))
|
|
197
194
|
|
|
198
|
-
report_verify_incremental(incremental, full, subset_size: subset.size, total: analyzed.size
|
|
195
|
+
report_verify_incremental(incremental, full, subset_size: subset.size, total: analyzed.size,
|
|
196
|
+
format: options.fetch(:format))
|
|
199
197
|
end
|
|
200
198
|
|
|
201
199
|
# ADR-46 — cross-process incremental analysis (`--incremental`). Derives the global fingerprint cheaply (no RBS
|
|
@@ -230,7 +228,8 @@ module Rigor
|
|
|
230
228
|
@err.puts("rigor: --incremental #{warm ? 'warm — reused cached diagnostics' : 'cold — full analysis'} " \
|
|
231
229
|
"(#{session.analyzed_files.size} files)")
|
|
232
230
|
emit_incremental_fact_surface_notes(session)
|
|
233
|
-
write_incremental_cache_stats(session, cache_root, store
|
|
231
|
+
write_incremental_cache_stats(session, cache_root, store, options.fetch(:format)) if
|
|
232
|
+
options.fetch(:cache_stats)
|
|
234
233
|
|
|
235
234
|
result = apply_baseline_filter(Analysis::Result.new(diagnostics: diagnostics, stats: nil), configuration,
|
|
236
235
|
options)
|
|
@@ -286,8 +285,8 @@ module Rigor
|
|
|
286
285
|
# does) plus the fact-surface status line, so an operator can see whether a full run was fact-surface
|
|
287
286
|
# driven. The plain `check` cache-stats path is bypassed by the incremental short-circuit, so it is
|
|
288
287
|
# emitted here.
|
|
289
|
-
def write_incremental_cache_stats(session, cache_root, store)
|
|
290
|
-
write_cache_stats(cache_root, store)
|
|
288
|
+
def write_incremental_cache_stats(session, cache_root, store, format)
|
|
289
|
+
write_cache_stats(cache_root, store, format)
|
|
291
290
|
status =
|
|
292
291
|
if !session.opaque_plugin_ids.empty?
|
|
293
292
|
"opaque (#{session.opaque_plugin_ids.sort.join(', ')})"
|
|
@@ -296,7 +295,7 @@ module Rigor
|
|
|
296
295
|
else
|
|
297
296
|
"unchanged"
|
|
298
297
|
end
|
|
299
|
-
|
|
298
|
+
side_output(format).puts(" plugin fact surface: #{status}")
|
|
300
299
|
end
|
|
301
300
|
|
|
302
301
|
def verify_full_diagnostics(configuration, paths)
|
|
@@ -310,11 +309,14 @@ module Rigor
|
|
|
310
309
|
end
|
|
311
310
|
end
|
|
312
311
|
|
|
313
|
-
|
|
312
|
+
# The OK line follows the same rule as the other side outputs (#493) — stdout under `text`, stderr
|
|
313
|
+
# under a machine format. The FAILED branch below has always gone to stderr: it is a failure
|
|
314
|
+
# report, and a run that produced one has no valid document to protect anyway.
|
|
315
|
+
def report_verify_incremental(incremental, full, subset_size:, total:, format: "text")
|
|
314
316
|
if incremental == full
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
317
|
+
side_output(format).puts("rigor: --verify-incremental OK — incremental " \
|
|
318
|
+
"(#{subset_size}/#{total} files re-analyzed, rest from cache) " \
|
|
319
|
+
"matches full (#{full.size} diagnostics)")
|
|
318
320
|
return 0
|
|
319
321
|
end
|
|
320
322
|
|
|
@@ -495,46 +497,46 @@ module Rigor
|
|
|
495
497
|
end
|
|
496
498
|
Options.add_editor_mode(opts, options)
|
|
497
499
|
opts.on("--baseline=PATH",
|
|
498
|
-
"
|
|
500
|
+
"load baseline from PATH (overrides .rigor.yml `baseline:`)") do |value|
|
|
499
501
|
options[:baseline] = value
|
|
500
502
|
end
|
|
501
503
|
opts.on("--no-baseline",
|
|
502
|
-
"
|
|
504
|
+
"ignore any configured baseline for this run") do
|
|
503
505
|
options[:baseline] = false
|
|
504
506
|
end
|
|
505
507
|
opts.on("--baseline-strict",
|
|
506
|
-
"
|
|
508
|
+
"fail the run on any baseline drift (CI gate)") do
|
|
507
509
|
options[:baseline_strict] = true
|
|
508
510
|
end
|
|
509
511
|
opts.on("--treat-all-as-inline-rbs",
|
|
510
|
-
"
|
|
512
|
+
"force-load rigor-rbs-inline with require_magic_comment: false") do
|
|
511
513
|
options[:treat_all_as_inline_rbs] = true
|
|
512
514
|
end
|
|
513
515
|
opts.on("--verify-incremental",
|
|
514
|
-
"
|
|
516
|
+
"assert incremental analysis matches a full run, then exit") do
|
|
515
517
|
options[:verify_incremental] = true
|
|
516
518
|
end
|
|
517
519
|
opts.on("--incremental",
|
|
518
|
-
"
|
|
520
|
+
"re-analyze only files changed since the last run (cross-process cache)") do
|
|
519
521
|
options[:incremental] = true
|
|
520
522
|
end
|
|
521
523
|
opts.on("--no-ci-detect",
|
|
522
|
-
"
|
|
524
|
+
"do not auto-emit CI-native output when a CI environment is detected") do
|
|
523
525
|
options[:ci_detect] = false
|
|
524
526
|
end
|
|
525
527
|
# ADR-50 § WD2 — `=[LIST]` (not ` [LIST]`) so a bare `--bleeding-edge` never swallows a following positional
|
|
526
528
|
# path: `rigor check --bleeding-edge lib` adopts the whole overlay and checks `lib`.
|
|
527
529
|
opts.on("--bleeding-edge=[LIST]",
|
|
528
|
-
"
|
|
530
|
+
"adopt the bleeding-edge overlay for this run " \
|
|
529
531
|
"(all features, or a comma-separated feature-id list)") do |value|
|
|
530
532
|
options[:bleeding_edge] = value.nil? || value.split(",").map(&:strip).reject(&:empty?)
|
|
531
533
|
end
|
|
532
534
|
opts.on("--no-bleeding-edge",
|
|
533
|
-
"
|
|
535
|
+
"ignore any configured bleeding_edge: selection for this run") do
|
|
534
536
|
options[:bleeding_edge] = false
|
|
535
537
|
end
|
|
536
538
|
opts.on("--no-tolerated-effects",
|
|
537
|
-
"
|
|
539
|
+
"check effect envelopes as if effects.tolerated: were empty") do
|
|
538
540
|
options[:no_tolerated_effects] = true
|
|
539
541
|
end
|
|
540
542
|
end
|
|
@@ -599,12 +601,13 @@ module Rigor
|
|
|
599
601
|
}]
|
|
600
602
|
end
|
|
601
603
|
|
|
602
|
-
def handle_clear_cache(cache_root)
|
|
604
|
+
def handle_clear_cache(cache_root, format)
|
|
605
|
+
out = side_output(format)
|
|
603
606
|
if File.directory?(cache_root)
|
|
604
607
|
FileUtils.rm_rf(cache_root)
|
|
605
|
-
|
|
608
|
+
out.puts("Cleared cache: #{cache_root}")
|
|
606
609
|
else
|
|
607
|
-
|
|
610
|
+
out.puts("Cache already empty: #{cache_root}")
|
|
608
611
|
end
|
|
609
612
|
end
|
|
610
613
|
|
|
@@ -772,42 +775,43 @@ module Rigor
|
|
|
772
775
|
Kernel.format("%.1f MB", bytes / 1_048_576.0)
|
|
773
776
|
end
|
|
774
777
|
|
|
775
|
-
def write_cache_stats(cache_root, runtime_store)
|
|
778
|
+
def write_cache_stats(cache_root, runtime_store, format)
|
|
776
779
|
inv = Cache::Store.disk_inventory(root: cache_root)
|
|
780
|
+
out = side_output(format)
|
|
777
781
|
|
|
778
|
-
|
|
779
|
-
|
|
782
|
+
out.puts("")
|
|
783
|
+
out.puts("Cache (root: #{inv.fetch(:root)})")
|
|
780
784
|
schema = inv.fetch(:schema_version)
|
|
781
|
-
|
|
782
|
-
write_disk_inventory(inv)
|
|
783
|
-
write_runtime_stats(runtime_store) if runtime_store
|
|
785
|
+
out.puts(" schema_version: #{schema.nil? ? 'absent' : schema}")
|
|
786
|
+
write_disk_inventory(inv, out)
|
|
787
|
+
write_runtime_stats(runtime_store, out) if runtime_store
|
|
784
788
|
end
|
|
785
789
|
|
|
786
|
-
def write_disk_inventory(inv)
|
|
790
|
+
def write_disk_inventory(inv, out)
|
|
787
791
|
if inv.fetch(:total_entries).zero?
|
|
788
|
-
|
|
792
|
+
out.puts(" (empty)")
|
|
789
793
|
return
|
|
790
794
|
end
|
|
791
795
|
|
|
792
|
-
|
|
796
|
+
out.puts(" #{inv.fetch(:total_entries)} entries, #{format_bytes(inv.fetch(:total_bytes))}")
|
|
793
797
|
inv.fetch(:producers).each do |producer|
|
|
794
798
|
bytes = format_bytes(producer.fetch(:bytes))
|
|
795
|
-
|
|
799
|
+
out.puts(" #{producer.fetch(:id)}: #{producer.fetch(:entries)} entries, #{bytes}")
|
|
796
800
|
end
|
|
797
801
|
end
|
|
798
802
|
|
|
799
|
-
def write_runtime_stats(store)
|
|
803
|
+
def write_runtime_stats(store, out)
|
|
800
804
|
stats = store.stats
|
|
801
805
|
hits = stats.fetch(:hits)
|
|
802
806
|
misses = stats.fetch(:misses)
|
|
803
807
|
writes = stats.fetch(:writes)
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
808
|
+
out.puts(" this run: #{hits} #{plural(hits, 'hit')}, " \
|
|
809
|
+
"#{misses} #{plural(misses, 'miss', 'misses')}, " \
|
|
810
|
+
"#{writes} #{plural(writes, 'write')}")
|
|
807
811
|
stats.fetch(:by_producer).each do |id, counts|
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
812
|
+
out.puts(" #{id}: #{counts.fetch(:hits)} #{plural(counts.fetch(:hits), 'hit')}, " \
|
|
813
|
+
"#{counts.fetch(:misses)} #{plural(counts.fetch(:misses), 'miss', 'misses')}, " \
|
|
814
|
+
"#{counts.fetch(:writes)} #{plural(counts.fetch(:writes), 'write')}")
|
|
811
815
|
end
|
|
812
816
|
end
|
|
813
817
|
|
|
@@ -822,6 +826,21 @@ module Rigor
|
|
|
822
826
|
format("%.1f MiB", bytes / (1024.0 * 1024.0))
|
|
823
827
|
end
|
|
824
828
|
|
|
829
|
+
# Where a human-readable side output goes (#493).
|
|
830
|
+
#
|
|
831
|
+
# `--format text` is prose already, so an extra block after the diagnostics is just more prose. Every
|
|
832
|
+
# other format is a machine contract — a SARIF document a code-scanning upload parses, a Checkstyle
|
|
833
|
+
# or JUnit XML tree, a GitLab Code Quality array — and appending `Cache (root: …)` to it produces a
|
|
834
|
+
# document the consumer rejects. Measured: all seven non-text formats were corrupted by
|
|
835
|
+
# `--cache-stats`, and JSON / SARIF / GitLab failed to parse at all.
|
|
836
|
+
#
|
|
837
|
+
# The information is not dropped, because it is what the user asked for by passing the flag; it moves
|
|
838
|
+
# to stderr, which is where this command already routes everything it says about a run rather than
|
|
839
|
+
# about the code.
|
|
840
|
+
def side_output(format)
|
|
841
|
+
format == "text" ? @out : @err
|
|
842
|
+
end
|
|
843
|
+
|
|
825
844
|
def write_result(result, format, coverage: nil, config_warnings: [])
|
|
826
845
|
case format
|
|
827
846
|
when "json"
|
|
@@ -14,7 +14,6 @@ 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"
|
|
18
17
|
require_relative "../scope"
|
|
19
18
|
require_relative "coverage_report"
|
|
20
19
|
require_relative "coverage_renderer"
|
|
@@ -55,11 +54,11 @@ module Rigor
|
|
|
55
54
|
DEFAULT_TEST_COMMAND = %w[bundle exec rake].freeze
|
|
56
55
|
|
|
57
56
|
# @return [Integer] CLI exit status.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
57
|
+
#
|
|
58
|
+
# Deferred YJIT enablement (Runtime::Jit) is armed by `CLI#dispatch` for every command, this
|
|
59
|
+
# one included: a scan long enough to amortize JIT compile cost enables mid-flight; a short one
|
|
60
|
+
# finishes first and never pays it.
|
|
61
|
+
def run
|
|
63
62
|
options = parse_options
|
|
64
63
|
return mutation_misuse_error if options[:mutation] && !options[:protection]
|
|
65
64
|
return with_tests_misuse_error if options[:with_tests] && !options[:mutation]
|
|
@@ -218,24 +217,23 @@ module Rigor
|
|
|
218
217
|
# class. Tier 1 seeds unconditionally because it only reclassifies sites it already counted; Tier 2 adds
|
|
219
218
|
# sites to a denominator `--threshold` gates CI on, which is why only the latter is gated on a feature id.
|
|
220
219
|
def scope_with_inferred_params(paths, configuration, environment, workers)
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
discovered = Inference::ScopeIndexer.discovered_classes_for_paths(paths)
|
|
225
|
-
seed[:discovered_classes] = discovered unless discovered.empty?
|
|
226
|
-
|
|
227
|
-
table = Inference::ParameterInferenceCollector.collect(
|
|
228
|
-
files: paths, environment: environment, target_ruby: configuration.target_ruby, workers: workers
|
|
220
|
+
CoverageScan.discovery_seeded_scope(
|
|
221
|
+
files: paths, configuration: configuration, environment: environment,
|
|
222
|
+
parameter_inference: true, workers: workers
|
|
229
223
|
)
|
|
230
|
-
seed[:param_inferred_types] = table unless table.empty?
|
|
231
|
-
|
|
232
|
-
return base if seed.empty?
|
|
233
|
-
|
|
234
|
-
base.with_discovery(base.discovery.with(**seed))
|
|
235
224
|
end
|
|
236
225
|
|
|
226
|
+
# Issue #686 — an unmeasured file fails the build the way a parse error already does, and for the same
|
|
227
|
+
# reason: the command could not measure part of what it was asked about, so its ratio is an answer to
|
|
228
|
+
# a smaller question than the user asked. Deliberately NOT routed through `--threshold`, which the
|
|
229
|
+
# line below applies to a ratio: a crashed file's mutants leave `killed + survived` at zero, the
|
|
230
|
+
# project ratio is then computed over the files that DID work, and a run that measured nothing at all
|
|
231
|
+
# came out at 1.0 and passed every threshold. It is also deliberately not the `harness_errors` count,
|
|
232
|
+
# which #264 keeps out of the exit code on purpose — a few rescued mutants are the transient that
|
|
233
|
+
# bucket exists to make visible, while a wholly unmeasured FILE is not a transient.
|
|
237
234
|
def determine_protection_exit(report, options)
|
|
238
235
|
return 1 unless report.parse_errors.empty?
|
|
236
|
+
return 1 if unmeasured_files_in(report).positive?
|
|
239
237
|
|
|
240
238
|
threshold = options[:threshold]
|
|
241
239
|
return 0 if threshold.nil?
|
|
@@ -243,6 +241,12 @@ module Rigor
|
|
|
243
241
|
report.ratio < threshold ? 1 : 0
|
|
244
242
|
end
|
|
245
243
|
|
|
244
|
+
# Issue #686 — the one reader of the count, shared with the stderr warning. The Tier-1 protection
|
|
245
|
+
# report has no such notion, so a report without the field reads as zero rather than erroring.
|
|
246
|
+
def unmeasured_files_in(report)
|
|
247
|
+
report.respond_to?(:unmeasured_files) ? report.unmeasured_files : 0
|
|
248
|
+
end
|
|
249
|
+
|
|
246
250
|
def usage_error
|
|
247
251
|
@err.puts("coverage: at least one path is required")
|
|
248
252
|
@err.puts(USAGE)
|
|
@@ -255,11 +259,11 @@ module Rigor
|
|
|
255
259
|
|
|
256
260
|
# The protection scan must see the same receiver types `rigor check` does — including plugin-contributed
|
|
257
261
|
# `dynamic_return` types (a controller's `params` → `ActionController::Parameters`, a `Model.where` →
|
|
258
|
-
# `ActiveRecord::Relation[Model]`).
|
|
259
|
-
#
|
|
260
|
-
#
|
|
261
|
-
#
|
|
262
|
-
#
|
|
262
|
+
# `ActiveRecord::Relation[Model]`). A bare RBS environment carries no plugin registry, so every plugin-typed
|
|
263
|
+
# receiver reads `Dynamic` and its dispatch site is miscounted as *unprotected* — a systematic undercount of
|
|
264
|
+
# what Rigor actually types on a plugin-using project. `ProjectContext` builds the plugin-aware environment
|
|
265
|
+
# (registry materialised + the per-run prepare pass that primes producers like the controller / model index)
|
|
266
|
+
# exactly as the LSP, the runner, and — since #513 — the precision path all do.
|
|
263
267
|
def plugin_aware_environment(configuration)
|
|
264
268
|
LanguageServer::ProjectContext.new(configuration: configuration).environment
|
|
265
269
|
end
|
|
@@ -9,6 +9,8 @@ require_relative "../protection/dependency_closure"
|
|
|
9
9
|
require_relative "../protection/discovery_seed"
|
|
10
10
|
require_relative "../protection/mutation_cache"
|
|
11
11
|
|
|
12
|
+
require_relative "measurement_integrity_warning"
|
|
13
|
+
|
|
12
14
|
module Rigor
|
|
13
15
|
class CLI
|
|
14
16
|
# ADR-63 Tier 2 + ADR-70 — the mutation-effectiveness and fused static∪dynamic protection paths, factored out of
|
|
@@ -43,17 +45,13 @@ module Rigor
|
|
|
43
45
|
|
|
44
46
|
private
|
|
45
47
|
|
|
46
|
-
#
|
|
48
|
+
# Both measurement-integrity warnings, and the escalation rule between them, live in
|
|
49
|
+
# {MeasurementIntegrityWarning}. Only the floor stays here: it is this command's policy, and
|
|
50
|
+
# `coverage_command_spec` reads it off this module.
|
|
51
|
+
#
|
|
52
|
+
# @param report [MutationProtectionReport, FusedProtectionReport]
|
|
47
53
|
def warn_harness_errors(report)
|
|
48
|
-
|
|
49
|
-
return if count < HARNESS_ERROR_WARN_FLOOR
|
|
50
|
-
|
|
51
|
-
@err.puts(
|
|
52
|
-
"coverage: #{count} mutants failed inside the measurement harness (\"harness_errors\", " \
|
|
53
|
-
"at/above the #{HARNESS_ERROR_WARN_FLOOR}-mutant floor) — excluded from the ratio like a " \
|
|
54
|
-
"parse-invalid mutant, but this many suggests a harness defect rather than one-off noise. " \
|
|
55
|
-
"Investigate before trusting --threshold on this run."
|
|
56
|
-
)
|
|
54
|
+
MeasurementIntegrityWarning.emit(report, err: @err, floor: HARNESS_ERROR_WARN_FLOOR)
|
|
57
55
|
end
|
|
58
56
|
|
|
59
57
|
# The cross-file knowledge Tier 2 measures with — the #253 gate, and the ONLY place in this feature that
|
|
@@ -262,13 +260,11 @@ module Rigor
|
|
|
262
260
|
# the two sets were assembled.
|
|
263
261
|
#
|
|
264
262
|
# The two cache phases each get their OWN {Cache::FileDigest.with_run} scope and the MEASUREMENT sits
|
|
265
|
-
# between them, deliberately: that scope installs a per-path digest memo
|
|
266
|
-
#
|
|
267
|
-
#
|
|
268
|
-
#
|
|
269
|
-
#
|
|
270
|
-
# the other side. The cost of two scopes is one extra SHA-256 per cached file, against a measurement
|
|
271
|
-
# that is hundreds of analyses.
|
|
263
|
+
# between them, deliberately: that scope installs a per-path digest memo. {Protection::ClosureKillOracle}
|
|
264
|
+
# now writes each mutant to a fresh block-scoped path, so a memo spanning the measurement could no
|
|
265
|
+
# longer serve one mutant another's digest — but keeping the measurement outside any memo scope stays
|
|
266
|
+
# correct by construction rather than by the oracle's path discipline, and costs only one extra
|
|
267
|
+
# SHA-256 per cached file against a measurement that is hundreds of analyses.
|
|
272
268
|
def measure_mutation_files(paths, cache:, scanner:, context:, configuration:, workers:)
|
|
273
269
|
cached = with_digest_run(configuration) { paths.to_h { |path| [path, cache.fetch(path)] }.compact }
|
|
274
270
|
pending = paths - cached.keys
|
|
@@ -4,7 +4,11 @@ require "prism"
|
|
|
4
4
|
|
|
5
5
|
require_relative "../configuration"
|
|
6
6
|
require_relative "../environment"
|
|
7
|
+
require_relative "../inference/parameter_inference_collector"
|
|
7
8
|
require_relative "../inference/precision_scanner"
|
|
9
|
+
require_relative "../inference/scope_indexer"
|
|
10
|
+
require_relative "../language_server/project_context"
|
|
11
|
+
require_relative "../protection/discovery_seed"
|
|
8
12
|
require_relative "../scope"
|
|
9
13
|
require_relative "coverage_report"
|
|
10
14
|
|
|
@@ -20,19 +24,56 @@ module Rigor
|
|
|
20
24
|
# @param files [Array<String>] explicit `.rb` file paths to scan.
|
|
21
25
|
# @param configuration [Rigor::Configuration]
|
|
22
26
|
# @return [Rigor::CLI::CoverageReport]
|
|
27
|
+
#
|
|
28
|
+
# Issue #513 — the environment is PLUGIN-AWARE (`LanguageServer::ProjectContext`, the same builder
|
|
29
|
+
# `--protection` uses), not the bare RBS environment this path carried until 2026-09-01. Without the
|
|
30
|
+
# plugin registry every plugin-typed receiver read `Dynamic` and the precision ratio understated the
|
|
31
|
+
# engine by ~1.2 points on a Rails target — the defect the protection path documented and fixed for
|
|
32
|
+
# itself on 2026-07-04, never applied here.
|
|
23
33
|
def precision_report(files:, configuration:)
|
|
24
|
-
scope =
|
|
34
|
+
scope = discovery_seeded_scope(
|
|
35
|
+
files: files,
|
|
36
|
+
configuration: configuration,
|
|
37
|
+
environment: LanguageServer::ProjectContext.new(configuration: configuration).environment,
|
|
38
|
+
parameter_inference: configuration.parameter_inference
|
|
39
|
+
)
|
|
25
40
|
scanner = Inference::PrecisionScanner.new(scope: scope)
|
|
26
41
|
accumulator = CoverageAccumulator.new
|
|
27
42
|
files.each { |path| scan_into(path, scanner, accumulator, configuration) }
|
|
28
43
|
accumulator.to_report(files, {})
|
|
29
44
|
end
|
|
30
45
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
46
|
+
# The cross-file facts a scan must see to report the types the engine actually infers, rather than the types a
|
|
47
|
+
# file-at-a-time walk can reach on its own.
|
|
48
|
+
#
|
|
49
|
+
# Issue #513 — the seed is the FULL check-walk bundle (`Protection::DiscoverySeed.discovery_tables`: classes,
|
|
50
|
+
# def nodes and sources for both kinds, superclasses, includes, visibilities, methods, and the Data / Struct
|
|
51
|
+
# member layouts), not just `discovered_classes`. The two-table seed #505 introduced closed the biggest part of
|
|
52
|
+
# the gap and left the rest: without the def-node / ancestry tables every cross-file call to a source-inferred
|
|
53
|
+
# project method measured as unresolved while `rigor check` resolves it — +0.27pp on redmine, +0.77pp on
|
|
54
|
+
# mastodon from the tables alone.
|
|
55
|
+
#
|
|
56
|
+
# `parameter_inference` is a parameter rather than a config read because the two callers want different answers:
|
|
57
|
+
# the precision lens must mirror the walk it describes (`check` leaves the table empty unless
|
|
58
|
+
# `parameter_inference:` is on, ADR-67 WD6a), while `--protection` seeds it unconditionally by ADR-67's own
|
|
59
|
+
# wiring — it only ever reclassifies sites it already counted.
|
|
60
|
+
#
|
|
61
|
+
# @return [Rigor::Scope]
|
|
62
|
+
def discovery_seeded_scope(files:, configuration:, environment:, parameter_inference:, workers: nil)
|
|
63
|
+
base = Scope.empty(environment: environment)
|
|
64
|
+
seed = Protection::DiscoverySeed.discovery_tables(files).dup
|
|
65
|
+
|
|
66
|
+
if parameter_inference
|
|
67
|
+
table = Inference::ParameterInferenceCollector.collect(
|
|
68
|
+
files: files, environment: environment, target_ruby: configuration.target_ruby,
|
|
69
|
+
workers: workers || configuration.parallel_workers
|
|
70
|
+
)
|
|
71
|
+
seed[:param_inferred_types] = table unless table.empty?
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
return base if seed.empty?
|
|
75
|
+
|
|
76
|
+
base.with_discovery(base.discovery.with(**seed))
|
|
36
77
|
end
|
|
37
78
|
|
|
38
79
|
# Parses one file and feeds the scan result (or a parse-error record) into `accumulator`. `scanner` /
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
class CLI
|
|
5
|
+
# Turns the documentation's own relative links into keys `rigor docs` can answer (#430).
|
|
6
|
+
#
|
|
7
|
+
# The manual and handbook link each other and the design corpus the way any prose does —
|
|
8
|
+
# `[ADR-103](../adr/103-effect-labels.md)`. Those links are correct in the repository and correct on
|
|
9
|
+
# GitHub, and they are the coupling between documents: they say which document explains what, and
|
|
10
|
+
# deleting them to leave a bare `ADR-103` would cost that and give nothing back.
|
|
11
|
+
#
|
|
12
|
+
# They are wrong in exactly one place: a reader looking at the file inside an installed gem, where
|
|
13
|
+
# `docs/adr/` was never packaged. Two earlier attempts fixed the file for that reader and made it
|
|
14
|
+
# worse for the others — rewriting every link to a `blob/master` URL hardcoded a host, an
|
|
15
|
+
# organisation and a branch in 297 places and sent a v0.3.5 reader to a document that had moved on;
|
|
16
|
+
# deleting the markup severed the coupling.
|
|
17
|
+
#
|
|
18
|
+
# So the source keeps its links and `rigor docs` rewrites them on the way out:
|
|
19
|
+
#
|
|
20
|
+
# [ADR-103](../adr/103-effect-labels.md) → [ADR-103][adr/103-effect-labels]
|
|
21
|
+
#
|
|
22
|
+
# The key is the repository path minus `docs/` and `.md`, which for a packaged page is exactly the
|
|
23
|
+
# name `rigor docs` already takes, so the reader can run what they are shown. For an unpackaged one
|
|
24
|
+
# the key still resolves — to a routed message naming the path and the repository, from the single
|
|
25
|
+
# constant below rather than from 297 rewritten links.
|
|
26
|
+
module DocLinks
|
|
27
|
+
REPOSITORY = "https://github.com/rigortype/rigor"
|
|
28
|
+
|
|
29
|
+
# `[label](target)` and `[label](target#anchor)`, skipping absolute URLs and bare anchors.
|
|
30
|
+
MARKDOWN_LINK = /\[([^\]]*)\]\((?!https?:|mailto:|#)([^)#\s]+)(#[^)\s]*)?\)/
|
|
31
|
+
private_constant :MARKDOWN_LINK
|
|
32
|
+
|
|
33
|
+
# Where the gem's `docs/` tree sits, so a rendered path can be made repository-relative.
|
|
34
|
+
GEM_ROOT = File.expand_path("../../..", __dir__)
|
|
35
|
+
private_constant :GEM_ROOT
|
|
36
|
+
|
|
37
|
+
module_function
|
|
38
|
+
|
|
39
|
+
# @param body [String] the page as it is stored.
|
|
40
|
+
# @param from [String] its absolute path, so relative targets resolve.
|
|
41
|
+
# @return [String] the page with every relative link turned into a `rigor docs` key.
|
|
42
|
+
def rewrite(body, from:)
|
|
43
|
+
dir = File.dirname(from)
|
|
44
|
+
body.gsub(MARKDOWN_LINK) do
|
|
45
|
+
label = Regexp.last_match(1)
|
|
46
|
+
target = Regexp.last_match(2)
|
|
47
|
+
anchor = Regexp.last_match(3)
|
|
48
|
+
key = key_for(target, dir)
|
|
49
|
+
key.nil? ? "[#{label}](#{target}#{anchor})" : "[#{label}][#{key}#{anchor}]"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Splits a key into its page and its section, for a reader who pasted one back.
|
|
54
|
+
def split_anchor(key)
|
|
55
|
+
page, anchor = key.to_s.split("#", 2)
|
|
56
|
+
[page, anchor]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# The key naming `target` as written in `dir`, or nil when it points outside the repository.
|
|
60
|
+
#
|
|
61
|
+
# A `.md` under `docs/` loses both the prefix and the extension, which is exactly the name
|
|
62
|
+
# `rigor docs` already answers — that is what lets a rendered page hand the reader something they
|
|
63
|
+
# can run. Everything else keeps its repository-relative path verbatim: a directory
|
|
64
|
+
# (`examples/rigor-deprecations/`), a source file (`spec/rigor/environment_spec.rb`), a root
|
|
65
|
+
# document (`CHANGELOG.md`). Those are equally routable and equally worth naming; only the
|
|
66
|
+
# round-trip differs, and {.repository_path} reverses both.
|
|
67
|
+
#
|
|
68
|
+
# `target` is taken as written rather than expanded, because `File.expand_path` drops the trailing
|
|
69
|
+
# slash that tells a directory link apart from a page link — prose that writes `[the ADRs](../adr/)`
|
|
70
|
+
# is pointing at a tree, and a key without the slash would claim there is a page there.
|
|
71
|
+
def key_for(target, dir)
|
|
72
|
+
relative = File.expand_path(target, dir).sub("#{GEM_ROOT}/", "")
|
|
73
|
+
return nil if relative.start_with?("/")
|
|
74
|
+
|
|
75
|
+
relative += "/" if target.end_with?("/")
|
|
76
|
+
return relative unless relative.start_with?("docs/") && relative.end_with?(".md")
|
|
77
|
+
|
|
78
|
+
relative.delete_prefix("docs/").delete_suffix(".md")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Repository directories a key may name verbatim. A key that starts with one of these already IS a
|
|
82
|
+
# path — `docs/adr`, `plugins/rigor-sorbet` — because {.key_for} only rewrites `.md` files under
|
|
83
|
+
# `docs/`. Anything else is a documentation key and gets its prefix and extension back.
|
|
84
|
+
#
|
|
85
|
+
# Keying on the ROOT rather than on "has an extension" is what makes a directory work: prose links
|
|
86
|
+
# a tree as often without a trailing slash (`../adr`) as with one, and no string test tells an
|
|
87
|
+
# extensionless file from a directory.
|
|
88
|
+
REPOSITORY_ROOTS = %w[docs examples plugins spec skills data tool bench apps].freeze
|
|
89
|
+
|
|
90
|
+
# The repository path a key names, reversing {.key_for}.
|
|
91
|
+
def repository_path(key)
|
|
92
|
+
return nil if key.nil? || key.empty? || key.include?("..")
|
|
93
|
+
return key if REPOSITORY_ROOTS.include?(key.split("/").first)
|
|
94
|
+
return key if File.extname(key) != ""
|
|
95
|
+
|
|
96
|
+
"docs/#{key}.md"
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "command"
|
|
4
|
+
require_relative "doc_links"
|
|
4
5
|
|
|
5
6
|
module Rigor
|
|
6
7
|
class CLI
|
|
@@ -118,7 +119,7 @@ module Rigor
|
|
|
118
119
|
# a UTF-8 header would set the output buffer to UTF-8 and clash with the body.
|
|
119
120
|
@out.puts("<!-- rigor docs #{doc.fetch(:name)} (rigortype #{Rigor::VERSION}, offline) -->")
|
|
120
121
|
@out.puts
|
|
121
|
-
@out.write(File.read(doc.fetch(:path)))
|
|
122
|
+
@out.write(DocLinks.rewrite(File.read(doc.fetch(:path)), from: doc.fetch(:path)))
|
|
122
123
|
0
|
|
123
124
|
end
|
|
124
125
|
|
|
@@ -138,6 +139,10 @@ module Rigor
|
|
|
138
139
|
# @return [Hash, Integer] the doc entry, or an error exit status
|
|
139
140
|
# after the error has been written to `@err`.
|
|
140
141
|
def resolve_doc(query)
|
|
142
|
+
# A key printed by a rendered page may carry the section it pointed at; resolve the page and say
|
|
143
|
+
# where to look inside it, rather than refusing a key this command handed out.
|
|
144
|
+
query, anchor = DocLinks.split_anchor(query)
|
|
145
|
+
@err.puts("rigor: (section `#{anchor}`)") if anchor && !anchor.empty?
|
|
141
146
|
docs = discover_docs
|
|
142
147
|
|
|
143
148
|
exact = docs.find { |doc| doc.fetch(:exact_aliases).include?(query) }
|
|
@@ -146,11 +151,36 @@ module Rigor
|
|
|
146
151
|
short = docs.select { |doc| doc.fetch(:short_name) == query }
|
|
147
152
|
case short.size
|
|
148
153
|
when 1 then short.first
|
|
149
|
-
when 0 then name_error(query)
|
|
154
|
+
when 0 then unpackaged_doc(query) || name_error(query)
|
|
150
155
|
else ambiguous_error(query, short)
|
|
151
156
|
end
|
|
152
157
|
end
|
|
153
158
|
|
|
159
|
+
# A key the rendered pages hand out for a document the gem does not carry (ADR-74 ships the manual and
|
|
160
|
+
# handbook; the ADR / specification / notes corpus and the repository trees stay out). The key is
|
|
161
|
+
# still answerable — it names a real path — so this routes rather than failing: a reader who followed
|
|
162
|
+
# `[ADR-103][adr/103-effect-labels]` out of `rigor docs` gets told where that document is, which is the
|
|
163
|
+
# whole point of handing them the key instead of a dead relative path or a `master` URL.
|
|
164
|
+
# Categories this command serves itself. A name that fails to resolve inside one of them is a typo,
|
|
165
|
+
# not an unpackaged document, and must reach the did-you-mean listing rather than be routed to a
|
|
166
|
+
# repository path that does not exist — `rigor docs list` and `rigor docs pathh` are the cases
|
|
167
|
+
# that found this.
|
|
168
|
+
PACKAGED_CATEGORIES = %w[manual handbook].freeze
|
|
169
|
+
private_constant :PACKAGED_CATEGORIES
|
|
170
|
+
|
|
171
|
+
def unpackaged_doc(query)
|
|
172
|
+
return nil unless query.include?("/")
|
|
173
|
+
return nil if PACKAGED_CATEGORIES.include?(query.split("/").first)
|
|
174
|
+
|
|
175
|
+
path = DocLinks.repository_path(query)
|
|
176
|
+
return nil if path.nil?
|
|
177
|
+
|
|
178
|
+
@err.puts("rigor: `#{query}` is not packaged with this gem — the manual and handbook are, the " \
|
|
179
|
+
"design records are not.")
|
|
180
|
+
@err.puts("rigor: it is `#{path}` in the Rigor repository (#{DocLinks::REPOSITORY}).")
|
|
181
|
+
1
|
|
182
|
+
end
|
|
183
|
+
|
|
154
184
|
# Every bundled doc, each carrying the aliases `rigor docs <name>` accepts and the category used by `--list`.
|
|
155
185
|
# `install.md` sits at the docs root (category `guide`); the rest are manual / handbook chapters.
|
|
156
186
|
def discover_docs
|