rigortype 0.2.9 → 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/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 +1 -0
- 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 +98 -4
- data/docs/manual/02-cli-reference.md +49 -12
- data/docs/manual/03-configuration.md +34 -2
- data/docs/manual/04-diagnostics.md +23 -0
- data/docs/manual/07-plugins.md +4 -4
- 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/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 +33 -3
- 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 +6 -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/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/rbs_loader.rb +164 -5
- data/lib/rigor/environment.rb +19 -27
- data/lib/rigor/inference/budget_trace.rb +77 -2
- data/lib/rigor/inference/closure_escape_analyzer.rb +14 -1
- data/lib/rigor/inference/def_handle.rb +23 -0
- data/lib/rigor/inference/def_node_resolver.rb +90 -0
- data/lib/rigor/inference/def_return_typer.rb +2 -1
- data/lib/rigor/inference/expression_typer.rb +361 -113
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +30 -5
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +200 -0
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +22 -16
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +33 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +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 +127 -17
- data/lib/rigor/inference/parameter_inference_collector.rb +15 -0
- data/lib/rigor/inference/project_patched_scanner.rb +2 -1
- data/lib/rigor/inference/rbs_type_translator.rb +8 -1
- data/lib/rigor/inference/scope_indexer.rb +451 -36
- data/lib/rigor/inference/statement_evaluator.rb +64 -15
- data/lib/rigor/inference/struct_fold_safety.rb +48 -11
- data/lib/rigor/inference/synthetic_method_scanner.rb +15 -14
- data/lib/rigor/inference/void_origin.rb +25 -0
- data/lib/rigor/inference/void_tail_summary.rb +220 -0
- data/lib/rigor/language_server/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/controller_discoverer.rb +5 -3
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +3 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +4 -2
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +4 -2
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +38 -6
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -1
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +3 -1
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +10 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +11 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +3 -1
- data/plugins/rigor-rails/lib/rigor-rails.rb +11 -12
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +3 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +3 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +7 -5
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +83 -9
- data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +9 -5
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +3 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +11 -0
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +4 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +4 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +3 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +74 -14
- data/sig/prism_node_children.rbs +9 -0
- data/sig/rigor/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 +42 -24
- data/lib/rigor/cli/ci_detector.rb +0 -89
|
@@ -8,12 +8,17 @@ require_relative "../configuration"
|
|
|
8
8
|
require_relative "../config_audit"
|
|
9
9
|
require_relative "../analysis/result"
|
|
10
10
|
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
|
+
# `load_check_dependencies` — so it must be a load-time require. It pulls only YAML, never the engine.
|
|
13
|
+
require_relative "../analysis/baseline"
|
|
14
|
+
require_relative "../runtime/jit"
|
|
12
15
|
require_relative "command"
|
|
13
16
|
require_relative "options"
|
|
14
17
|
require_relative "diagnostic_formats"
|
|
15
|
-
require_relative "ci_detector"
|
|
16
|
-
|
|
18
|
+
require_relative "../ci_detector"
|
|
19
|
+
# ADR-87 WD4 — `coverage_scan` and `check_runner_factory` both pull the inference engine, so they are required
|
|
20
|
+
# lazily (in `compute_coverage` / `build_check_runner`) rather than at load time: an ordinary check whose
|
|
21
|
+
# result the run-cache probe serves must reach the hit verdict without `rigor/inference` in $LOADED_FEATURES.
|
|
17
22
|
|
|
18
23
|
module Rigor
|
|
19
24
|
class CLI
|
|
@@ -29,18 +34,34 @@ module Rigor
|
|
|
29
34
|
# concerns that are clearer read together than split across micro-classes.
|
|
30
35
|
class CheckCommand < Command # rubocop:disable Metrics/ClassLength
|
|
31
36
|
# @return [Integer] CLI exit status.
|
|
32
|
-
def run # rubocop:disable Metrics/AbcSize
|
|
33
|
-
|
|
37
|
+
def run # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
38
|
+
# Arm deferred YJIT enablement before any analysis work: the deadline
|
|
39
|
+
# thread only fires once a run outlasts the amortization window, so a
|
|
40
|
+
# short check finishes before it ever pays JIT compile cost while a
|
|
41
|
+
# long run JITs its dominant tail (Runtime::Jit).
|
|
42
|
+
Runtime::Jit.enable_after(Runtime::Jit.deadline_seconds)
|
|
43
|
+
# ADR-87 WD4 — parse options + resolve config WITHOUT the inference engine, so the run-cache hit probe
|
|
44
|
+
# can run first. The heavy engine (`load_check_dependencies`) loads only on a miss / non-cacheable run.
|
|
34
45
|
options = parse_check_options
|
|
35
46
|
buffer = Options.resolve_buffer_binding(options, err: @err)
|
|
36
47
|
return CLI::EXIT_USAGE if buffer == :usage_error
|
|
37
48
|
|
|
38
49
|
configuration = load_check_configuration(options)
|
|
39
50
|
configuration = apply_bleeding_edge_override(configuration, options)
|
|
51
|
+
conflict = parameter_inference_incremental_conflict(configuration, options)
|
|
52
|
+
return conflict unless conflict.nil?
|
|
53
|
+
|
|
40
54
|
config_warnings = warn_unresolved_config(configuration)
|
|
41
55
|
cache_root = configuration.cache_path
|
|
42
56
|
handle_clear_cache(cache_root) if options.fetch(:clear_cache)
|
|
43
57
|
|
|
58
|
+
# ADR-87 WD4 — try to serve the whole run from the ADR-45 cache before booting the engine. A hit boots
|
|
59
|
+
# only CLI + config + cache + digest code (no `rigor/inference`), skipping the plugin prepass + env
|
|
60
|
+
# build entirely.
|
|
61
|
+
probed = try_run_cache_hit(configuration, options, buffer, cache_root)
|
|
62
|
+
return finalize_cache_hit(probed, configuration, options, config_warnings) unless probed.nil?
|
|
63
|
+
|
|
64
|
+
load_check_dependencies
|
|
44
65
|
special = dispatch_special_check_mode(configuration, options, cache_root)
|
|
45
66
|
return special unless special.nil?
|
|
46
67
|
|
|
@@ -66,23 +87,113 @@ module Rigor
|
|
|
66
87
|
|
|
67
88
|
private
|
|
68
89
|
|
|
90
|
+
# ADR-87 WD4 — attempt the boot-slimming run-cache hit. Returns the cached {Analysis::Result} (severity
|
|
91
|
+
# profile applied, no stats — matching a cache-served `Runner#run`) on a hit, or nil to fall through to
|
|
92
|
+
# the full engine path. Only an ordinary sequential check whose result IS cache-served the same way is
|
|
93
|
+
# eligible; every other mode (below) declines so the full path handles it unchanged.
|
|
94
|
+
def try_run_cache_hit(configuration, options, buffer, cache_root)
|
|
95
|
+
return nil unless run_cache_hit_eligible?(configuration, options, buffer)
|
|
96
|
+
|
|
97
|
+
require_relative "../analysis/run_cache_probe"
|
|
98
|
+
Analysis::RunCacheProbe.new(
|
|
99
|
+
configuration: configuration, cache_root: cache_root, explain: options.fetch(:explain)
|
|
100
|
+
).serve(@argv.empty? ? configuration.paths : @argv)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# The probe is eligible only for a run the ADR-45 result cache actually serves: a writable cache (not
|
|
104
|
+
# `--no-cache`), no editor buffer, sequential (pool runs are not result-cacheable), and no mode needing
|
|
105
|
+
# the engine (`--coverage` / `--incremental` / `--verify-incremental`) or the runner's own reporting
|
|
106
|
+
# (`--cache-stats`, the `RIGOR_*_TRACE` dev probes). A wrongly-permitted run still MISSES (the entry was
|
|
107
|
+
# never written its way) and falls through — the gate is an optimisation, never a soundness boundary.
|
|
108
|
+
def run_cache_hit_eligible?(configuration, options, buffer)
|
|
109
|
+
buffer.nil? &&
|
|
110
|
+
!options.fetch(:no_cache) && !options.fetch(:coverage) && !options.fetch(:cache_stats) &&
|
|
111
|
+
!options.fetch(:incremental) && !options.fetch(:verify_incremental) &&
|
|
112
|
+
!pool_workers_configured?(options, configuration) &&
|
|
113
|
+
ENV["RIGOR_BUDGET_TRACE"].to_s.empty? && ENV["RIGOR_HEAP_TRACE"].to_s.empty?
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Any signal that per-file analysis would run in a worker pool (CLI `--workers`, the env override, or the
|
|
117
|
+
# config default). Deliberately over-declines (a false positive only forgoes the fast lane); mirrors
|
|
118
|
+
# {CheckRunnerFactory.resolve_workers} without requiring it (that pulls in the engine).
|
|
119
|
+
def pool_workers_configured?(options, configuration)
|
|
120
|
+
cli = options[:workers]
|
|
121
|
+
return Integer(cli).positive? if cli
|
|
122
|
+
|
|
123
|
+
env = ENV.fetch("RIGOR_RACTOR_WORKERS", nil)
|
|
124
|
+
return Integer(env).positive? if env && !env.empty?
|
|
125
|
+
|
|
126
|
+
configuration.parallel_workers.positive?
|
|
127
|
+
rescue ArgumentError
|
|
128
|
+
false
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# ADR-87 WD4 — the engine-free tail for a cache-served hit: baseline filter + output + exit code,
|
|
132
|
+
# mirroring the full path's tail but without stats (a hit collected none), trace appendices (they read
|
|
133
|
+
# engine counters), or eviction (deferred to the next miss). `raw_result` is pre-baseline so the strict
|
|
134
|
+
# gate reads the same diagnostics the full path would.
|
|
135
|
+
def finalize_cache_hit(raw_result, configuration, options, config_warnings)
|
|
136
|
+
result = apply_baseline_filter(raw_result, configuration, options)
|
|
137
|
+
write_result(result, options.fetch(:format), config_warnings: config_warnings)
|
|
138
|
+
emit_ci_detected_output(result, options)
|
|
139
|
+
|
|
140
|
+
exit_code = result.success? ? 0 : 1
|
|
141
|
+
exit_code = 1 if baseline_strict_violation?(raw_result.diagnostics, configuration, options)
|
|
142
|
+
exit_code
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# ADR-67 WD6c — `parameter_inference:` and the ADR-46 incremental modes are mutually exclusive in slice 1.
|
|
146
|
+
# The parameter table introduces cross-file edges (a caller's argument types drive a callee's body
|
|
147
|
+
# diagnostics) that the per-file dependency recorder does not yet carry, so a cached file whose parameter
|
|
148
|
+
# seeds changed would serve a stale result. Refuse with a message naming the ADR-46 gap rather than
|
|
149
|
+
# silently producing unsound incremental diagnostics; the edge wiring is the named follow-up. Returns the
|
|
150
|
+
# usage exit code on a conflict, or nil to proceed. `--verify-incremental` (the acceptance gate that runs
|
|
151
|
+
# incremental analysis) is caught here too.
|
|
152
|
+
def parameter_inference_incremental_conflict(configuration, options)
|
|
153
|
+
return nil unless configuration.parameter_inference
|
|
154
|
+
return nil unless options.fetch(:incremental) || options.fetch(:verify_incremental)
|
|
155
|
+
|
|
156
|
+
flag = options.fetch(:incremental) ? "--incremental" : "--verify-incremental"
|
|
157
|
+
@err.puts("rigor: parameter_inference: cannot combine with #{flag} — the call-site parameter table " \
|
|
158
|
+
"introduces cross-file caller→callee edges the ADR-46 incremental dependency graph does not " \
|
|
159
|
+
"yet record, so a cached file could serve a stale diagnostic. Run a full check (drop " \
|
|
160
|
+
"#{flag}), or disable parameter_inference: for this run.")
|
|
161
|
+
CLI::EXIT_USAGE
|
|
162
|
+
end
|
|
163
|
+
|
|
69
164
|
# ADR-46 — the two incremental-analysis check modes both fully handle the run and return an exit code (so `run`
|
|
70
165
|
# short-circuits); returns nil for an ordinary check.
|
|
71
166
|
def dispatch_special_check_mode(configuration, options, cache_root)
|
|
72
|
-
return run_verify_incremental(configuration) if options.fetch(:verify_incremental)
|
|
167
|
+
return run_verify_incremental(configuration, options, cache_root) if options.fetch(:verify_incremental)
|
|
73
168
|
return run_incremental_check(configuration, options, cache_root) if options.fetch(:incremental)
|
|
74
169
|
|
|
75
170
|
nil
|
|
76
171
|
end
|
|
77
172
|
|
|
173
|
+
# ADR-85 WD1 — the persistent cache the incremental session threads into each internal Runner so plugin
|
|
174
|
+
# `#prepare` producers (the ADR-9/#74/ADR-60 WD3 record-and-validate caches) and the RBS environment serve
|
|
175
|
+
# from disk across processes / rechecks rather than recomputing every invocation. Honors `--no-cache` (nil
|
|
176
|
+
# = no store), mirroring {CheckRunnerFactory}. The whole-run ADR-45 result cache stays inert on these runs
|
|
177
|
+
# — `Runner#run_result_cacheable?` excludes `record_dependencies` / `analyze_only`.
|
|
178
|
+
def incremental_cache_store(configuration, options, cache_root)
|
|
179
|
+
return nil if options.fetch(:no_cache)
|
|
180
|
+
|
|
181
|
+
Cache::Store.new(root: cache_root, max_bytes: configuration.cache_max_bytes)
|
|
182
|
+
end
|
|
183
|
+
|
|
78
184
|
# ADR-46 — the incremental-analysis acceptance gate. Runs a baseline analysis (recording cross-file dependencies),
|
|
79
185
|
# then re-analyzes a representative subset of files and serves the rest from the per-file cache (the body tier),
|
|
80
186
|
# and asserts the merged diagnostics are byte-identical to a full `--no-cache` analysis. A mismatch means the
|
|
81
187
|
# incremental machinery would serve a stale — manufactured — diagnostic, the soundness failure this gate exists to
|
|
82
188
|
# catch. Prints a one-line PASS (exit 0) or the differing diagnostics (exit 1).
|
|
83
|
-
def run_verify_incremental(configuration)
|
|
189
|
+
def run_verify_incremental(configuration, options, cache_root)
|
|
84
190
|
paths = @argv.empty? ? nil : @argv
|
|
85
|
-
|
|
191
|
+
# ADR-85 WD1 — thread the store so the gate also asserts the cache-served producer path stays
|
|
192
|
+
# byte-identical to the uncached full run (`verify_full_diagnostics` builds a nil-store runner).
|
|
193
|
+
session = Analysis::IncrementalSession.new(
|
|
194
|
+
configuration: configuration, paths: paths,
|
|
195
|
+
cache_store: incremental_cache_store(configuration, options, cache_root)
|
|
196
|
+
)
|
|
86
197
|
session.baseline
|
|
87
198
|
analyzed = session.analyzed_files
|
|
88
199
|
|
|
@@ -101,18 +212,30 @@ module Rigor
|
|
|
101
212
|
# updated snapshot for the next invocation. Diagnostics are identical to a full run (the `--verify-incremental`
|
|
102
213
|
# gate enforces this); the win is skipping per-file inference for unchanged files.
|
|
103
214
|
def run_incremental_check(configuration, options, cache_root)
|
|
215
|
+
require_relative "check_runner_factory"
|
|
104
216
|
paths = @argv.empty? ? nil : @argv
|
|
105
|
-
probe = Analysis::Runner.new(configuration: configuration, cache_store: nil)
|
|
106
|
-
files = paths ? probe.analysis_file_set(paths) : probe.analysis_file_set
|
|
107
217
|
fingerprint = Cache::IncrementalSnapshot.fingerprint(
|
|
108
218
|
configuration: configuration, roots: paths || configuration.paths
|
|
109
219
|
)
|
|
110
220
|
snapshot = Cache::IncrementalSnapshot.new(root: cache_root)
|
|
111
|
-
|
|
221
|
+
store = incremental_cache_store(configuration, options, cache_root)
|
|
222
|
+
session = Analysis::IncrementalSession.new(
|
|
223
|
+
configuration: configuration, paths: paths,
|
|
224
|
+
cache_store: store,
|
|
225
|
+
# ADR-46 — thread the same worker-count precedence the standard `check` path uses
|
|
226
|
+
# (CLI `--workers` > `RIGOR_RACTOR_WORKERS` > `parallel.workers:` > 0) so the recheck's closure
|
|
227
|
+
# re-analysis parallelises; the fork pool marshals dependency records back so the graph is sound.
|
|
228
|
+
workers: CheckRunnerFactory.resolve_workers(options, configuration)
|
|
229
|
+
)
|
|
112
230
|
|
|
113
231
|
diagnostics, warm = session.run_incremental(snapshot: snapshot, fingerprint: fingerprint)
|
|
232
|
+
# The banner's file count comes from the session's analyzed set (cold analyses all; a warm recheck's
|
|
233
|
+
# `@analyzed` advances to the current file set), so a dedicated probe Runner + `Dir.glob` is no longer
|
|
234
|
+
# built just to size the banner (recon §3 — the analysis tree was expanded three times per recheck).
|
|
114
235
|
@err.puts("rigor: --incremental #{warm ? 'warm — reused cached diagnostics' : 'cold — full analysis'} " \
|
|
115
|
-
"(#{
|
|
236
|
+
"(#{session.analyzed_files.size} files)")
|
|
237
|
+
emit_incremental_fact_surface_notes(session)
|
|
238
|
+
write_incremental_cache_stats(session, cache_root, store) if options.fetch(:cache_stats)
|
|
116
239
|
|
|
117
240
|
result = apply_baseline_filter(Analysis::Result.new(diagnostics: diagnostics, stats: nil), configuration,
|
|
118
241
|
options)
|
|
@@ -120,6 +243,40 @@ module Rigor
|
|
|
120
243
|
result.success? ? 0 : 1
|
|
121
244
|
end
|
|
122
245
|
|
|
246
|
+
# ADR-88 WD1 — a one-line stderr note when the plugin fact surface (an ADR-9 fact, an ADR-60 producer
|
|
247
|
+
# value, or an `incremental_state_fingerprint` hook) forced a full run: either it CHANGED since the
|
|
248
|
+
# snapshot (a Sorbet sig edit, a schema change) or a contributing plugin declares NO fingerprint surface
|
|
249
|
+
# (opaque — a full run every invocation). Both are the sound direction; the note explains why a recheck
|
|
250
|
+
# was not warm.
|
|
251
|
+
def emit_incremental_fact_surface_notes(session)
|
|
252
|
+
opaque = session.opaque_plugin_ids
|
|
253
|
+
unless opaque.empty?
|
|
254
|
+
@err.puts("rigor: --incremental plugin#{'s' unless opaque.size == 1} #{opaque.sort.join(', ')} " \
|
|
255
|
+
"contribute call-site types with no incremental fingerprint surface; the snapshot cannot " \
|
|
256
|
+
"be reused (full analysis every run).")
|
|
257
|
+
end
|
|
258
|
+
return unless session.fact_surface_invalidated? && opaque.empty?
|
|
259
|
+
|
|
260
|
+
@err.puts("rigor: --incremental plugin fact surface changed since the snapshot; ran a full analysis.")
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# ADR-88 WD1 — `--incremental --cache-stats` surfaces the on-disk cache inventory (as a plain `check`
|
|
264
|
+
# does) plus the fact-surface status line, so an operator can see whether a full run was fact-surface
|
|
265
|
+
# driven. The plain `check` cache-stats path is bypassed by the incremental short-circuit, so it is
|
|
266
|
+
# emitted here.
|
|
267
|
+
def write_incremental_cache_stats(session, cache_root, store)
|
|
268
|
+
write_cache_stats(cache_root, store)
|
|
269
|
+
status =
|
|
270
|
+
if !session.opaque_plugin_ids.empty?
|
|
271
|
+
"opaque (#{session.opaque_plugin_ids.sort.join(', ')})"
|
|
272
|
+
elsif session.fact_surface_invalidated?
|
|
273
|
+
"changed — snapshot invalidated"
|
|
274
|
+
else
|
|
275
|
+
"unchanged"
|
|
276
|
+
end
|
|
277
|
+
@out.puts(" plugin fact surface: #{status}")
|
|
278
|
+
end
|
|
279
|
+
|
|
123
280
|
def verify_full_diagnostics(configuration, paths)
|
|
124
281
|
runner = Analysis::Runner.new(configuration: configuration, cache_store: nil)
|
|
125
282
|
(paths ? runner.run(paths) : runner.run).diagnostics
|
|
@@ -227,6 +384,7 @@ module Rigor
|
|
|
227
384
|
end
|
|
228
385
|
|
|
229
386
|
def build_check_runner(configuration:, options:, buffer:, cache_root:)
|
|
387
|
+
require_relative "check_runner_factory"
|
|
230
388
|
CheckRunnerFactory.build(
|
|
231
389
|
configuration: configuration, options: options,
|
|
232
390
|
buffer: buffer, cache_root: cache_root
|
|
@@ -391,7 +549,10 @@ module Rigor
|
|
|
391
549
|
end
|
|
392
550
|
|
|
393
551
|
def inject_treat_all_as_inline_rbs(entries)
|
|
394
|
-
|
|
552
|
+
# Single-homed with the WD2 auto-wire gate (`Configuration.rbs_inline_plugin_entry?`) so the flag and
|
|
553
|
+
# the default agree on what "already listed" means; a pre-existing entry is removed so this one's
|
|
554
|
+
# `require_magic_comment: false` wins unconditionally.
|
|
555
|
+
filtered = entries.reject { |entry| Configuration.rbs_inline_plugin_entry?(entry) }
|
|
395
556
|
filtered + [{
|
|
396
557
|
"gem" => "rigor-rbs-inline",
|
|
397
558
|
"id" => "rbs-inline",
|
|
@@ -399,18 +560,6 @@ module Rigor
|
|
|
399
560
|
}]
|
|
400
561
|
end
|
|
401
562
|
|
|
402
|
-
def rigor_rbs_inline_entry?(entry)
|
|
403
|
-
case entry
|
|
404
|
-
when String
|
|
405
|
-
entry == "rigor-rbs-inline"
|
|
406
|
-
when Hash
|
|
407
|
-
string_keyed = entry.to_h { |k, v| [k.to_s, v] }
|
|
408
|
-
string_keyed["gem"] == "rigor-rbs-inline" || string_keyed["id"] == "rbs-inline"
|
|
409
|
-
else
|
|
410
|
-
false
|
|
411
|
-
end
|
|
412
|
-
end
|
|
413
|
-
|
|
414
563
|
def handle_clear_cache(cache_root)
|
|
415
564
|
if File.directory?(cache_root)
|
|
416
565
|
FileUtils.rm_rf(cache_root)
|
|
@@ -451,6 +600,42 @@ module Rigor
|
|
|
451
600
|
@err.puts(" recursion-fixpoint-cap hits: #{counts[Inference::BudgetTrace::RECURSION_FIXPOINT_CAP]}")
|
|
452
601
|
@err.puts(" block-writeback-cap hits: #{counts[Inference::BudgetTrace::BLOCK_WRITEBACK_CAP]}")
|
|
453
602
|
write_budget_distributions
|
|
603
|
+
write_memo_profile(counts)
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
# Dumps the ADR-57 return-memo profile (WD0). Entries / hit rate / body evals / the non-store split, then
|
|
607
|
+
# the top signatures by body-eval count with their distinct-memo-key counts — the evidence for choosing
|
|
608
|
+
# between a finalization-aware taint gate and memo-key normalization. `--workers 0` for exact counts.
|
|
609
|
+
def write_memo_profile(counts)
|
|
610
|
+
bt = Inference::BudgetTrace
|
|
611
|
+
hits = counts[bt::MEMO_HITS]
|
|
612
|
+
misses = counts[bt::MEMO_MISSES]
|
|
613
|
+
consults = hits + misses
|
|
614
|
+
rate = consults.positive? ? format("%.1f%%", 100.0 * hits / consults) : "n/a"
|
|
615
|
+
@err.puts("")
|
|
616
|
+
@err.puts("Return-memo profile (RIGOR_BUDGET_TRACE; --workers 0 for an exact count)")
|
|
617
|
+
@err.puts(" infer entries: #{counts[bt::MEMO_ENTRIES]}")
|
|
618
|
+
@err.puts(" memo consults: #{consults} (hits #{hits} / misses #{misses}; hit rate #{rate})")
|
|
619
|
+
@err.puts(" body evals: #{counts[bt::MEMO_BODY_EVALS]}")
|
|
620
|
+
@err.puts(" non-stored: on-stack #{counts[bt::MEMO_REFUSE_ON_STACK]} " \
|
|
621
|
+
"unroll-in-flight #{counts[bt::MEMO_REFUSE_UNROLL]} " \
|
|
622
|
+
"consult-tainted #{counts[bt::MEMO_REFUSE_CONSULT_TAINTED]} " \
|
|
623
|
+
"transient-tainted #{counts[bt::MEMO_REFUSE_TRANSIENT]}")
|
|
624
|
+
write_memo_signature_table
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
# Top 15 signatures by body-eval (compute) count, each with its distinct-memo-key count — a signature
|
|
628
|
+
# whose distinct-key count dwarfs its evals is arg-granularity thrash (the key-normalization candidate).
|
|
629
|
+
def write_memo_signature_table
|
|
630
|
+
bt = Inference::BudgetTrace
|
|
631
|
+
evals = bt.distribution(bt::MEMO_BODY_EVAL_BY_SIGNATURE)
|
|
632
|
+
return if evals.empty?
|
|
633
|
+
|
|
634
|
+
keys = bt.distribution(bt::MEMO_DISTINCT_KEY_BY_SIGNATURE)
|
|
635
|
+
@err.puts(" top signatures by body-eval count (evals / distinct-keys / signature):")
|
|
636
|
+
evals.sort_by { |sig, n| [-n, sig] }.first(15).each do |sig, n|
|
|
637
|
+
@err.puts(" #{n.to_s.rjust(8)} #{keys.fetch(sig, 0).to_s.rjust(6)} #{sig}")
|
|
638
|
+
end
|
|
454
639
|
end
|
|
455
640
|
|
|
456
641
|
# Dumps the read-only size distributions (ADR-41 Slice 2a). These observe how large unions actually get, with no
|
|
@@ -488,7 +673,6 @@ module Rigor
|
|
|
488
673
|
def load_check_dependencies
|
|
489
674
|
require_relative "../analysis/runner"
|
|
490
675
|
require_relative "../analysis/buffer_binding"
|
|
491
|
-
require_relative "../analysis/baseline"
|
|
492
676
|
require_relative "../cache/store"
|
|
493
677
|
start_heap_trace_if_requested
|
|
494
678
|
end
|
|
@@ -625,6 +809,7 @@ module Rigor
|
|
|
625
809
|
def compute_coverage(runner, configuration, options)
|
|
626
810
|
return nil unless options.fetch(:coverage)
|
|
627
811
|
|
|
812
|
+
require_relative "coverage_scan"
|
|
628
813
|
files = @argv.empty? ? runner.analysis_file_set : runner.analysis_file_set(@argv)
|
|
629
814
|
CoverageScan.precision_report(files: files, configuration: configuration)
|
|
630
815
|
end
|
|
@@ -682,7 +867,7 @@ module Rigor
|
|
|
682
867
|
return unless options.fetch(:ci_detect)
|
|
683
868
|
return unless options.fetch(:format) == "text"
|
|
684
869
|
|
|
685
|
-
platform =
|
|
870
|
+
platform = CiDetector.detect
|
|
686
871
|
return if platform.nil?
|
|
687
872
|
|
|
688
873
|
if platform.native_stdout?
|
|
@@ -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"
|
|
@@ -53,7 +54,11 @@ module Rigor
|
|
|
53
54
|
DEFAULT_TEST_COMMAND = %w[bundle exec rake].freeze
|
|
54
55
|
|
|
55
56
|
# @return [Integer] CLI exit status.
|
|
56
|
-
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)
|
|
57
62
|
options = parse_options
|
|
58
63
|
return mutation_misuse_error if options[:mutation] && !options[:protection]
|
|
59
64
|
return with_tests_misuse_error if options[:with_tests] && !options[:mutation]
|
|
@@ -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))
|