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
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Analysis
|
|
5
|
+
# Issues #665 / #674 / #683 / #686 / #696 — the ONE definition of "an internal failure made this run
|
|
6
|
+
# report less than it should have".
|
|
7
|
+
#
|
|
8
|
+
# Every member of that family shares a defect: the run still exits 0 and its diagnostic list still
|
|
9
|
+
# looks like an answer, so an absence assertion (`not_to include(...)`, `be_empty`, `all(eq(...))`)
|
|
10
|
+
# holds on it and a comparison of two such runs (`baseline == mutant`, `recheck == full_run`) reports
|
|
11
|
+
# "no difference" — because both sides carry the same synthetic row, not because the code agreed. Four
|
|
12
|
+
# separate consumers needed to recognise these shapes, and before this module each recognised them by
|
|
13
|
+
# matching a diagnostic MESSAGE with its own string literal. A fourth copy is what this exists to stop:
|
|
14
|
+
# a reworded message silently disarms whichever copies were not updated, and nothing goes red.
|
|
15
|
+
#
|
|
16
|
+
# ## The three shapes
|
|
17
|
+
#
|
|
18
|
+
# - `:check_rule` — message begins `internal analyzer error`, `rule: nil`. Built by
|
|
19
|
+
# `Runner#analyze_file_body` / `WorkerSession#analyze_body`'s `rescue StandardError`.
|
|
20
|
+
# - `:plugin` — `severity: :error`, `source_family: :plugin_loader`, `rule: "runtime-error"`. Built by
|
|
21
|
+
# `Runner#collect_plugin_diagnostics` / `Runner::ProjectPrePasses#invoke_plugin_prepare`.
|
|
22
|
+
# - `:rbs_build` — `rule` in {RBS_BUILD_FAILURE_RULES}. Recorded by `Environment::RbsLoader`'s
|
|
23
|
+
# env-build and definition-build rescues, surfaced by {Runner::DiagnosticAggregator}.
|
|
24
|
+
#
|
|
25
|
+
# The `:plugin` shape is keyed on the structured `(severity, source_family, rule)` triple rather than on
|
|
26
|
+
# the bare rule name: a plugin is free to define its OWN `"runtime-error"` (or `"load-error"`) under its
|
|
27
|
+
# own `source_family: "plugin.<id>"`, and several specs legitimately assert on one. Only the
|
|
28
|
+
# `:plugin_loader` family paired with `"runtime-error"` is the isolation envelope's own row.
|
|
29
|
+
#
|
|
30
|
+
# ## Only ONE shape discards a file's analysis
|
|
31
|
+
#
|
|
32
|
+
# {.discards_file_analysis?} answers true for `:check_rule` alone, and the distinction is not pedantry —
|
|
33
|
+
# it decides whether a consumer may still read the run's diagnostics.
|
|
34
|
+
#
|
|
35
|
+
# - `:check_rule` — `Runner#analyze_file_body` rescues around the WHOLE per-file body, so what comes
|
|
36
|
+
# back is `[crash_row]` and every diagnostic that file would have produced is gone. Nothing is left to
|
|
37
|
+
# read.
|
|
38
|
+
# - `:plugin` — `collect_plugin_diagnostics` replaces only the raising PLUGIN's contribution;
|
|
39
|
+
# `CheckRules.diagnose` has already returned, and `invoke_plugin_prepare` adds one row at `.rigor.yml`
|
|
40
|
+
# and lets the run proceed. The builtin rules ran, and their diagnostics are all still there. A
|
|
41
|
+
# consumer that refuses the whole run over this throws away a real measurement — and because the
|
|
42
|
+
# prepare row is appended to every sequential run, it would refuse every run for the life of the
|
|
43
|
+
# process.
|
|
44
|
+
# - `:rbs_build` — the analysis ran to completion and every rule fired, over a type universe missing one
|
|
45
|
+
# class (`rbs.coverage.definition-build-failed`) or all of them
|
|
46
|
+
# (`rbs.coverage.environment-build-failed`). A degradation the user causes and the diagnostic itself
|
|
47
|
+
# reports; a project can sit on it for a release while it fixes its `sig/`.
|
|
48
|
+
#
|
|
49
|
+
# The two consumer tiers therefore differ on purpose. The ADR-69 kill oracles arm `:check_rule` only:
|
|
50
|
+
# refusing a run they could still have measured is the same "manufactures work" error as scoring an
|
|
51
|
+
# unmeasured mutant a survivor, pointed the other way (issue #686 review). The spec harness additionally
|
|
52
|
+
# arms `:plugin`, which is suite POLICY rather than a claim about the diagnostics — no spec has a reason
|
|
53
|
+
# to want a plugin crashing under it, and `allow_plugin_crash:` is the opt-out for the handful whose
|
|
54
|
+
# subject IS the isolation envelope.
|
|
55
|
+
module CrashSignature
|
|
56
|
+
# The `rescue StandardError` in `Runner#analyze_file_body` / `WorkerSession#analyze_body` folds a
|
|
57
|
+
# raising check rule (or a plugin's node-rule contribution) into ONE diagnostic per file with this
|
|
58
|
+
# message prefix. Matched on the prefix alone, exactly as the pre-#696 guards did: adding a `rule.nil?`
|
|
59
|
+
# conjunct would NARROW the match, and the whole point of a guard against a bug is that it stays armed
|
|
60
|
+
# when the shape shifts.
|
|
61
|
+
CHECK_RULE_MESSAGE_PREFIX = "internal analyzer error"
|
|
62
|
+
|
|
63
|
+
PLUGIN_SOURCE_FAMILY = :plugin_loader
|
|
64
|
+
PLUGIN_RULE = "runtime-error"
|
|
65
|
+
|
|
66
|
+
# The `rbs.coverage.*` rules that mean declared types went missing from this run — an env-wide
|
|
67
|
+
# collapse and its per-class sibling (#696). Ordered widest consequence last, the way the two rows sit
|
|
68
|
+
# in `docs/type-specification/diagnostic-policy.md`.
|
|
69
|
+
RBS_BUILD_FAILURE_RULES = %w[
|
|
70
|
+
rbs.coverage.definition-build-failed
|
|
71
|
+
rbs.coverage.environment-build-failed
|
|
72
|
+
].freeze
|
|
73
|
+
|
|
74
|
+
# The one reason that means a file's whole diagnostic list was replaced by a crash row. `:plugin` and
|
|
75
|
+
# `:rbs_build` both leave a readable run behind — see the class doc.
|
|
76
|
+
DISCARDS_FILE_ANALYSIS_REASON = :check_rule
|
|
77
|
+
|
|
78
|
+
module_function
|
|
79
|
+
|
|
80
|
+
# @param diagnostic [Rigor::Analysis::Diagnostic]
|
|
81
|
+
# @return [Symbol, nil] `:check_rule`, `:plugin`, `:rbs_build`, or nil for an ordinary diagnostic.
|
|
82
|
+
def reason(diagnostic)
|
|
83
|
+
return :check_rule if diagnostic.message.to_s.start_with?(CHECK_RULE_MESSAGE_PREFIX)
|
|
84
|
+
return :plugin if plugin_isolation_row?(diagnostic)
|
|
85
|
+
return :rbs_build if RBS_BUILD_FAILURE_RULES.include?(diagnostic.rule)
|
|
86
|
+
|
|
87
|
+
nil
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# True when `diagnostic` is the rescue row that REPLACED a file's analysis — the only shape after which
|
|
91
|
+
# the run's diagnostics say nothing about the code. NOT a general "something went wrong" predicate; see
|
|
92
|
+
# the class doc for why `:plugin` and `:rbs_build` are excluded.
|
|
93
|
+
#
|
|
94
|
+
# @param diagnostic [Rigor::Analysis::Diagnostic]
|
|
95
|
+
def discards_file_analysis?(diagnostic)
|
|
96
|
+
reason(diagnostic) == DISCARDS_FILE_ANALYSIS_REASON
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# A one-line "<reason> at <path>:<line>: <message>" for a failure message, so whoever reads the raise
|
|
100
|
+
# sees which shape fired and where without re-deriving it.
|
|
101
|
+
#
|
|
102
|
+
# @param diagnostic [Rigor::Analysis::Diagnostic]
|
|
103
|
+
# @return [String]
|
|
104
|
+
def describe(diagnostic)
|
|
105
|
+
"#{reason(diagnostic) || :unknown} at #{diagnostic.path}:#{diagnostic.line}: #{diagnostic.message}"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def plugin_isolation_row?(diagnostic)
|
|
109
|
+
diagnostic.severity == :error &&
|
|
110
|
+
diagnostic.source_family == PLUGIN_SOURCE_FAMILY &&
|
|
111
|
+
diagnostic.rule == PLUGIN_RULE
|
|
112
|
+
end
|
|
113
|
+
private_class_method :plugin_isolation_row?
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -168,9 +168,17 @@ module Rigor
|
|
|
168
168
|
accumulate_read(accumulator, path, symbol)
|
|
169
169
|
end
|
|
170
170
|
|
|
171
|
-
# Records a cross-file
|
|
172
|
-
#
|
|
173
|
-
|
|
171
|
+
# Records a NAME-KEYED cross-file dependency: this consumer's answer depends on what the project makes
|
|
172
|
+
# of `name` under `kind`, so a recheck must re-analyze it when that changes. The recorded key is
|
|
173
|
+
# inverted into `negative_dependents` and matched against the producer for its kind.
|
|
174
|
+
#
|
|
175
|
+
# For `:method` / `:class` / `:toplevel` this is recorded only on a MISS ({read_missing}) — a resolved
|
|
176
|
+
# method or class already carries a positive edge to its definition site, which covers every later
|
|
177
|
+
# change to it. Issue #644's `:constant` kind is recorded on a HIT as well, because a constant's
|
|
178
|
+
# published value is a function of the WHOLE project's write set for the name, not of the one file that
|
|
179
|
+
# currently wins: a second declarer appearing in a file the reader has no edge to must drop the name to
|
|
180
|
+
# `Dynamic` at that reader, and only a name-keyed edge can see it.
|
|
181
|
+
def read_name(kind, name)
|
|
174
182
|
accumulator = Thread.current[KEY]
|
|
175
183
|
return if accumulator.nil?
|
|
176
184
|
|
|
@@ -179,6 +187,13 @@ module Rigor
|
|
|
179
187
|
accumulator.missing.add(entry)
|
|
180
188
|
end
|
|
181
189
|
|
|
190
|
+
# The miss-only spelling of {read_name}: a cross-file lookup of `name` (kind `:method` / `:class` /
|
|
191
|
+
# `:toplevel` / …) that resolved to nothing. Kept as the name the internal spec's negative-edge
|
|
192
|
+
# contract uses.
|
|
193
|
+
def read_missing(kind, name)
|
|
194
|
+
read_name(kind, name)
|
|
195
|
+
end
|
|
196
|
+
|
|
182
197
|
# The positive-edge aggregation shared by {read_site} and {replay}.
|
|
183
198
|
def accumulate_read(accumulator, path, symbol)
|
|
184
199
|
accumulator.sources << path
|
|
@@ -139,7 +139,8 @@ module Rigor
|
|
|
139
139
|
def descend_class_or_module(node, qualified_prefix, in_singleton_class, accumulator, budget)
|
|
140
140
|
name = Source::ConstantPath.qualified_name_or_nil(node.constant_path)
|
|
141
141
|
if name && node.body
|
|
142
|
-
|
|
142
|
+
child_prefix = Source::ConstantPath.declaration_prefix(qualified_prefix, node.constant_path)
|
|
143
|
+
walk_node(node.body, child_prefix, in_singleton_class, accumulator, budget)
|
|
143
144
|
else
|
|
144
145
|
walk_children(node, qualified_prefix, in_singleton_class, accumulator, budget)
|
|
145
146
|
end
|
|
@@ -134,14 +134,25 @@ module Rigor
|
|
|
134
134
|
end
|
|
135
135
|
|
|
136
136
|
# Text rendering for `rigor check`. The qualified rule identifier (per ADR-2 § "Plugin Diagnostic
|
|
137
|
-
# Provenance" — `plugin.<id>.<rule>`, `rbs_extended.<rule>`,
|
|
138
|
-
#
|
|
139
|
-
#
|
|
140
|
-
#
|
|
137
|
+
# Provenance" — `call.undefined-method`, `plugin.<id>.<rule>`, `rbs_extended.<rule>`,
|
|
138
|
+
# `generated.<provider>.<rule>`) is appended in brackets.
|
|
139
|
+
#
|
|
140
|
+
# Slice 5 (v0.1.0) introduced the bracket for non-builtin families only, "without changing the layout
|
|
141
|
+
# for built-in rules" — a layout-conservatism call made when provenance was the point, not a judgment
|
|
142
|
+
# that the identifier is noise. The consequence outlived the reason (#431): the exception covers the
|
|
143
|
+
# rules `docs/manual/04-diagnostics.md` tells the reader to suppress with `# rigor:disable <id>` and
|
|
144
|
+
# to key `severity_profile:` on, so the default output was the one place the identifier could not be
|
|
145
|
+
# read. A run could not be grepped for a rule either — the effect-system walkthrough grepped
|
|
146
|
+
# `effect\.` over a whole Redmine run, got nothing, and concluded the feature was broken.
|
|
147
|
+
#
|
|
148
|
+
# The cost is bounded and was measured before the change: on Redmine every diagnostic carries an
|
|
149
|
+
# identifier, only 13 distinct ones appear across the project, and the suffix adds 14 characters to a
|
|
150
|
+
# 165-character median line.
|
|
151
|
+
#
|
|
152
|
+
# `rule` is nil for diagnostics no rule produced — parse errors, path errors, internal analyzer
|
|
153
|
+
# errors — and those stay unsuffixed, because there is nothing to suppress or configure.
|
|
141
154
|
def to_s
|
|
142
155
|
base = "#{path}:#{line}:#{column}: #{severity}: #{message}"
|
|
143
|
-
return base if source_family == DEFAULT_SOURCE_FAMILY
|
|
144
|
-
|
|
145
156
|
qualified = qualified_rule
|
|
146
157
|
return base if qualified.nil?
|
|
147
158
|
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "path_expansion"
|
|
4
|
+
require_relative "run_cache_key"
|
|
5
|
+
require_relative "../cache/store"
|
|
6
|
+
require_relative "../cache/file_digest"
|
|
7
|
+
require_relative "../effects/identity"
|
|
8
|
+
require_relative "../effects/plugin_facts"
|
|
9
|
+
require_relative "../effects/registry"
|
|
10
|
+
require_relative "../plugin"
|
|
11
|
+
require_relative "../plugin/loader"
|
|
12
|
+
require_relative "../plugin/services"
|
|
13
|
+
require_relative "../reflection"
|
|
14
|
+
require_relative "../type/combinator"
|
|
15
|
+
|
|
16
|
+
module Rigor
|
|
17
|
+
module Analysis
|
|
18
|
+
# ADR-104 — the boot-slimming probe for the effects surfaces, the shape ADR-87 WD4's
|
|
19
|
+
# {RunCacheProbe} gave `rigor check`.
|
|
20
|
+
#
|
|
21
|
+
# A warm `rigor effects` had almost no work left after #475 and #482 and still cost 2–2.5× a warm
|
|
22
|
+
# `rigor check`, because it loaded the inference engine to compute two cache keys and adopt two
|
|
23
|
+
# cached values. This serves the report from the #482 summary entry — the propagated table and the
|
|
24
|
+
# unit sources — loading configuration, the cache, the plugin loader and the `effects/*` value
|
|
25
|
+
# layer, and never `rigor/inference`.
|
|
26
|
+
#
|
|
27
|
+
# ## What makes an engine-free key possible
|
|
28
|
+
#
|
|
29
|
+
# The effects identity ({Effects::Identity.descriptor}) is the run's diagnostics key descriptor plus
|
|
30
|
+
# the vocabulary version, the catalogue identity, the `effects:` digest and {Effects::PluginFacts}'
|
|
31
|
+
# own digest. The first is what {RunCacheProbe} already computes without a loader; the rest are pure
|
|
32
|
+
# functions of shipped data, configuration, and the plugins' class-level declarations — a plugin's
|
|
33
|
+
# contributions are fixed by `init`, which the loader runs, so `#prepare` (the expensive half) is
|
|
34
|
+
# not needed. `PluginFacts#digest` is computed before the project's superclass table is even
|
|
35
|
+
# assigned to it, so the discovery tables a probe cannot build do not participate.
|
|
36
|
+
#
|
|
37
|
+
# ## Why a decline is always safe
|
|
38
|
+
#
|
|
39
|
+
# Every reason to decline — a key this path cannot reproduce, a miss, a stale dependency — ends in
|
|
40
|
+
# the caller running the full `Analysis::Runner`, which answers exactly as it does today. The one
|
|
41
|
+
# reproducibility gap is deliberate and inherited: a project whose plugins synthesise virtual RBS
|
|
42
|
+
# gets a key without the `rbs.virtual_rbs` slot the runner writes, so it misses here rather than
|
|
43
|
+
# matching something it should not.
|
|
44
|
+
#
|
|
45
|
+
# The rule the surfaces are held to (ADR-104's criterion): a probe may serve a surface only when
|
|
46
|
+
# every answer that surface can give is reproducible from stored values and declarations alone.
|
|
47
|
+
# `rigor effects` and the snapshot verbs qualify because their output is a pure function of the
|
|
48
|
+
# table, the sources, the configuration and the vocabulary — the envelope diagnostics the full path
|
|
49
|
+
# also computes are discarded by every one of them.
|
|
50
|
+
class EffectsCacheProbe
|
|
51
|
+
# What a served run hands back: the two cached tables, plus the vocabulary the caller would
|
|
52
|
+
# otherwise rebuild (the probe loaded the plugins to key the entry, so it already knows it).
|
|
53
|
+
Served = Data.define(:table, :sources, :registry, :plugin_facts)
|
|
54
|
+
|
|
55
|
+
# @param configuration [Rigor::Configuration] with effects already enabled by the caller.
|
|
56
|
+
# @param cache_root [String, nil]
|
|
57
|
+
def initialize(configuration:, cache_root:)
|
|
58
|
+
@configuration = configuration
|
|
59
|
+
@cache_root = cache_root
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @param paths [Array<String>] the analysed set — `configuration.paths` unioned with any path
|
|
63
|
+
# arguments, exactly what {CLI::EffectsCommand#analyze} hands the runner, because the analysed
|
|
64
|
+
# set is part of the diagnostics key.
|
|
65
|
+
# @return [Served, nil] nil to decline, on any failure whatsoever.
|
|
66
|
+
def serve(paths)
|
|
67
|
+
return nil if @cache_root.nil? || !@configuration.effects_check?
|
|
68
|
+
|
|
69
|
+
descriptor = effects_descriptor(paths)
|
|
70
|
+
return nil if descriptor.nil?
|
|
71
|
+
|
|
72
|
+
summary = validated_summary(descriptor)
|
|
73
|
+
return nil if summary.nil?
|
|
74
|
+
|
|
75
|
+
table, sources, = summary
|
|
76
|
+
Served.new(table: table, sources: sources, registry: registry, plugin_facts: plugin_facts)
|
|
77
|
+
rescue StandardError
|
|
78
|
+
nil
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def effects_descriptor(paths)
|
|
84
|
+
files = PathExpansion.ruby_files(paths, @configuration.exclude_patterns)
|
|
85
|
+
base = RunCacheKey.descriptor(
|
|
86
|
+
configuration: @configuration, files: files, explain: false,
|
|
87
|
+
rbs_config_entries: RunCacheKey.libraries_config_entries(@configuration)
|
|
88
|
+
)
|
|
89
|
+
return nil if base.nil?
|
|
90
|
+
|
|
91
|
+
Effects::Identity.descriptor(base: base, configuration: @configuration, registry: registry,
|
|
92
|
+
plugin_facts: plugin_facts)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# The same shape guard the runner's own read half applies, for the same reason: a miss, a stale
|
|
96
|
+
# dependency, a corrupt entry and a value of the wrong shape all mean "run it properly".
|
|
97
|
+
def validated_summary(descriptor)
|
|
98
|
+
store = Cache::Store.new(root: @cache_root, max_bytes: @configuration.cache_max_bytes)
|
|
99
|
+
cached = Cache::FileDigest.with_run(strict: @configuration.cache_validation_strict?) do
|
|
100
|
+
store.peek_validated(
|
|
101
|
+
producer_id: RunCacheKey::RUN_EFFECTS_TABLE_PRODUCER_ID, key_descriptor: descriptor
|
|
102
|
+
)
|
|
103
|
+
end
|
|
104
|
+
return nil unless cached.is_a?(Array) && cached.length == 3
|
|
105
|
+
return nil unless cached[0].is_a?(Effects::EffectTable) && cached[1].is_a?(Hash)
|
|
106
|
+
|
|
107
|
+
cached
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Loaded once, and shared by the key and the answer. `#prepare` is deliberately not run: a
|
|
111
|
+
# plugin's effect contributions are fixed by `init`, and `#prepare` is the half that parses
|
|
112
|
+
# routes and walks worker trees — the cost this probe exists to skip.
|
|
113
|
+
def plugin_facts
|
|
114
|
+
return @plugin_facts if defined?(@plugin_facts)
|
|
115
|
+
|
|
116
|
+
@plugin_facts = Effects::PluginFacts.build(plugin_registry)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def plugin_registry
|
|
120
|
+
services = Plugin::Services.new(reflection: Reflection, type: Type::Combinator,
|
|
121
|
+
configuration: @configuration, cache_store: nil)
|
|
122
|
+
Plugin::Loader.load(configuration: @configuration, services: services)
|
|
123
|
+
rescue StandardError, ScriptError
|
|
124
|
+
nil
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def registry
|
|
128
|
+
@registry ||= Effects::Registry.for_configuration(@configuration, plugin_facts: plugin_facts)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -120,6 +120,34 @@ module Rigor
|
|
|
120
120
|
appeared.freeze
|
|
121
121
|
end
|
|
122
122
|
|
|
123
|
+
# Issue #644 — the qualified constant names whose PUBLICATION could have moved in this edit: the names
|
|
124
|
+
# whose per-file census descriptor differs between the before- and after-state of any file in `paths`.
|
|
125
|
+
# The value-constant twin of {appeared_classes}, and deliberately NOT its analogue — a class either
|
|
126
|
+
# exists or does not, whereas a constant's published answer is a function of the whole project's write
|
|
127
|
+
# set for the name, so a *name* diff is the wrong question. All four ways the answer moves are a
|
|
128
|
+
# descriptor change and none of them is an appearance:
|
|
129
|
+
#
|
|
130
|
+
# - the value edited (`[50]` -> `[7]`),
|
|
131
|
+
# - the write becoming unpublishable or publishable again (`[50]` <-> `:unpublishable`),
|
|
132
|
+
# - a SECOND declarer appearing, which drops the name to `Dynamic` (`{}` -> `{NAME => …}` in that file),
|
|
133
|
+
# - a second declarer going away, which restores the precise answer.
|
|
134
|
+
#
|
|
135
|
+
# `paths` MUST therefore include removed files as well as changed and added ones; a removed file's
|
|
136
|
+
# after-state is simply absent, so its whole before-census counts as changed. Returns a frozen Set of
|
|
137
|
+
# qualified-name Strings. `before` / `after` map a path to `{name => descriptor}`.
|
|
138
|
+
def changed_constant_publications(paths, before, after)
|
|
139
|
+
moved = Set.new
|
|
140
|
+
paths.each do |path|
|
|
141
|
+
was = before[path] || {}
|
|
142
|
+
now = after[path] || {}
|
|
143
|
+
# `eql?`, not `==`: `[1] == [1.0]` is TRUE, so `LIMIT = 1` becoming `LIMIT = 1.0` would move the
|
|
144
|
+
# published type (`Constant[1]` -> `Constant[1.0]`) while this diff called it unchanged. The
|
|
145
|
+
# declaration signature backstops that case today; the producer must not depend on it.
|
|
146
|
+
(was.keys | now.keys).each { |name| moved << name unless was[name].eql?(now[name]) }
|
|
147
|
+
end
|
|
148
|
+
moved.freeze
|
|
149
|
+
end
|
|
150
|
+
|
|
123
151
|
# ADR-46 slice 3 — the consumers to re-check because a name they looked up and *missed* (a negative
|
|
124
152
|
# dependency) now resolves. `keys` is the set of negative-dependency keys (`"toplevel:foo"` /
|
|
125
153
|
# `"method:C#m"`) the appeared symbols would satisfy; `negative_dependents` maps each key to the Set of
|
|
@@ -98,6 +98,10 @@ module Rigor
|
|
|
98
98
|
@missing = {} # consumer => Set<"kind:name"> it looked up and missed
|
|
99
99
|
@negative_dependents = {} # "kind:name" => Set<consumer> (inverted @missing)
|
|
100
100
|
@class_decls = {} # path => Set<qualified class name declared in the file>
|
|
101
|
+
# Issue #644 — the value-constant twin: path => { qualified constant name => publication descriptor }.
|
|
102
|
+
# Diffed on a recheck to find every constant whose PUBLISHED answer could have moved, which satisfies
|
|
103
|
+
# the `constant:` edges a reader records at every reference, resolved or not.
|
|
104
|
+
@constant_decls = {}
|
|
101
105
|
# ADR-89 WD2 — per-def observed-key return summaries: [path, symbol] => { keys:, returns:, effects: }.
|
|
102
106
|
# Harvested from the ADR-84 return memo after each run; drives the behavioural-stability gate.
|
|
103
107
|
@return_summaries = {}
|
|
@@ -259,6 +263,7 @@ module Rigor
|
|
|
259
263
|
declaration_signatures = (summary && summary[:declaration_signatures]) || {}
|
|
260
264
|
new_fps = symbol_fingerprints_from_index(scan_index)
|
|
261
265
|
new_class_decls = class_declarations_from_index(scan_index)
|
|
266
|
+
new_constant_decls = constant_declarations_from_index(scan_index)
|
|
262
267
|
changed_pairs = Incremental.changed_symbol_pairs(changed, @symbol_fingerprints, new_fps)
|
|
263
268
|
# ADR-89 WD1 — a changed file whose DECLARATION signature (per-def parameter shape / visibility /
|
|
264
269
|
# ancestry / member layout / def line, bodies excluded) is byte-identical to the snapshot is
|
|
@@ -275,7 +280,8 @@ module Rigor
|
|
|
275
280
|
# minus those stable pairs, and only it (not `changed_pairs`) drives the symbol-dependent fan-out.
|
|
276
281
|
symbol_pairs = behaviourally_unstable_pairs(changed_pairs, unstable, scan_index) | param_pairs
|
|
277
282
|
base = dependents_base(unstable, symbol_pairs)
|
|
278
|
-
closure = base | changed.to_set | added.to_set |
|
|
283
|
+
closure = base | changed.to_set | added.to_set |
|
|
284
|
+
negative_affected(scan, removed, new_fps, new_class_decls, new_constant_decls)
|
|
279
285
|
closure = param_seed_closure(closure, param_files)
|
|
280
286
|
removed.each { |path| closure |= @dependents[path] || Set.new }
|
|
281
287
|
closure.freeze
|
|
@@ -550,12 +556,7 @@ module Rigor
|
|
|
550
556
|
@symbol_sources = payload.symbol_sources || {}
|
|
551
557
|
@ancestry_sources = payload.ancestry_sources || {}
|
|
552
558
|
@symbol_fingerprints = payload.symbol_fingerprints || {}
|
|
553
|
-
|
|
554
|
-
# only loses the appeared-symbol re-check refinement; such a snapshot is never loaded by a slice-3+
|
|
555
|
-
# engine because the engine version + schema is part of the snapshot fingerprint, and
|
|
556
|
-
# `--verify-incremental` backstops any residual under-capture, so it is never unsound).
|
|
557
|
-
@missing = payload.missing || {}
|
|
558
|
-
@class_decls = payload.class_decls || {}
|
|
559
|
+
restore_structural_declarations(payload)
|
|
559
560
|
@return_summaries = payload.return_summaries || {}
|
|
560
561
|
@param_table = payload.param_table || {} # ADR-67 WD6c lift — the seeds the cache was built under.
|
|
561
562
|
# ADR-103 WD13 / #382 — the effects sidecar rides its own identity, so a snapshot whose summaries
|
|
@@ -569,12 +570,24 @@ module Rigor
|
|
|
569
570
|
@negative_dependents = Incremental.invert(@missing)
|
|
570
571
|
end
|
|
571
572
|
|
|
573
|
+
# ADR-46 slice 3 / issue #644 — the structural tier's negative-edge state: the per-consumer missed
|
|
574
|
+
# lookups plus the two per-file DECLARATION sets (`class_decls`, `constant_decls`) whose diff says what
|
|
575
|
+
# APPEARED in an edit. Absent in an older snapshot → empty, which only loses the appeared-symbol
|
|
576
|
+
# refinement; such a snapshot is never loaded by a newer engine (the version + SCHEMA are part of the
|
|
577
|
+
# fingerprint) and `--verify-incremental` backstops any residual under-capture. Extracted to keep
|
|
578
|
+
# {#restore} under its ABC budget.
|
|
579
|
+
def restore_structural_declarations(payload)
|
|
580
|
+
@missing = payload.missing || {}
|
|
581
|
+
@class_decls = payload.class_decls || {}
|
|
582
|
+
@constant_decls = payload.constant_decls || {}
|
|
583
|
+
end
|
|
584
|
+
|
|
572
585
|
def to_payload
|
|
573
586
|
Cache::IncrementalSnapshot::Payload.new(
|
|
574
587
|
cache: @cache, sources: @sources, digests: @digests, analyzed: @analyzed,
|
|
575
588
|
symbol_sources: @symbol_sources, ancestry_sources: @ancestry_sources,
|
|
576
589
|
symbol_fingerprints: @symbol_fingerprints, missing: @missing,
|
|
577
|
-
class_decls: @class_decls, seed_bundles: @seed_bundles,
|
|
590
|
+
class_decls: @class_decls, constant_decls: @constant_decls, seed_bundles: @seed_bundles,
|
|
578
591
|
plugin_fact_digest: @plugin_fact_digest,
|
|
579
592
|
return_summaries: marshal_safe_return_summaries,
|
|
580
593
|
param_table: marshal_safe_param_table,
|
|
@@ -754,6 +767,7 @@ module Rigor
|
|
|
754
767
|
# Wholesale replace (the subset runner's pre-pass is complete): a file that lost its last class must
|
|
755
768
|
# drop out of the map so a later re-add registers as an appearance.
|
|
756
769
|
@class_decls = runner.class_declarations
|
|
770
|
+
@constant_decls = runner.constant_declarations
|
|
757
771
|
end
|
|
758
772
|
|
|
759
773
|
# Per-symbol body fingerprints from the pre-parsed `index` (the shared scan-summary def-index — Prism
|
|
@@ -792,11 +806,18 @@ module Rigor
|
|
|
792
806
|
# a prior missed lookup. Maps each appeared `"ClassName#method"` to the negative-dependency key it
|
|
793
807
|
# would satisfy (`toplevel:foo` for a top-level def, `method:C#m` otherwise), then unions the recorded
|
|
794
808
|
# negative-dependents of those keys.
|
|
795
|
-
def negative_affected(changed, new_fingerprints, new_class_decls)
|
|
809
|
+
def negative_affected(changed, removed, new_fingerprints, new_class_decls, new_constant_decls)
|
|
796
810
|
appeared_methods = Incremental.appeared_symbols(changed, @symbol_fingerprints, new_fingerprints)
|
|
797
811
|
appeared_classes = Incremental.appeared_classes(changed, @class_decls, new_class_decls)
|
|
812
|
+
# Issue #644 — a constant whose PUBLICATION moved satisfies the `constant:` kind, keyed on the same
|
|
813
|
+
# last segment the consumer recorded. REMOVED files join the diff here and nowhere else: deleting a
|
|
814
|
+
# second declarer restores the precise answer, and no other producer input can see that.
|
|
815
|
+
moved_constants = Incremental.changed_constant_publications(
|
|
816
|
+
changed + removed, @constant_decls, new_constant_decls
|
|
817
|
+
)
|
|
798
818
|
keys = appeared_methods.map { |symbol| negative_key_for(symbol) }
|
|
799
819
|
keys.concat(appeared_classes.map { |klass| "class:#{klass.split('::').last}" })
|
|
820
|
+
keys.concat(moved_constants.map { |name| "constant:#{name.split('::').last}" })
|
|
800
821
|
Incremental.negative_closure(keys, @negative_dependents)
|
|
801
822
|
end
|
|
802
823
|
|
|
@@ -812,6 +833,20 @@ module Rigor
|
|
|
812
833
|
result.transform_values(&:freeze).freeze
|
|
813
834
|
end
|
|
814
835
|
|
|
836
|
+
# Issue #644 — the per-file PUBLICATION CENSUS of the pre-parsed `index`:
|
|
837
|
+
# `{ path => { constant name => descriptor } }`, the twin of {#class_declarations_from_index}. The
|
|
838
|
+
# descriptor is what the diff compares — see {Incremental.changed_constant_publications} for why a bare
|
|
839
|
+
# name set cannot answer the question.
|
|
840
|
+
def constant_declarations_from_index(index)
|
|
841
|
+
return {} if index.nil?
|
|
842
|
+
|
|
843
|
+
result = Hash.new { |hash, key| hash[key] = {} }
|
|
844
|
+
index[:constant_writes].each do |name, by_path|
|
|
845
|
+
by_path.each { |path, descriptor| result[path][name] = descriptor }
|
|
846
|
+
end
|
|
847
|
+
result.transform_values(&:freeze).freeze
|
|
848
|
+
end
|
|
849
|
+
|
|
815
850
|
# ADR-89 WD2 — the (live, freshly parsed) def node for a `"Class#method"` / `"Class.method"` symbol in
|
|
816
851
|
# the changed-set scan index, or nil. Instance methods live under `def_nodes`, singleton under
|
|
817
852
|
# `singleton_def_nodes`, both keyed by class name then method Symbol.
|
|
@@ -151,7 +151,7 @@ module Rigor
|
|
|
151
151
|
# value actually moves (a new action, a changed sig), which is what a cached diagnostic can depend on;
|
|
152
152
|
# a value-preserving edit keeps the recheck incremental. Returns true when the plugin declared at least
|
|
153
153
|
# one producer (a surface); a producer whose value cannot be digested marks the plugin opaque.
|
|
154
|
-
# rubocop:disable Naming/PredicateMethod -- reports has-surface AND appends parts (a side-effecting builder)
|
|
154
|
+
# rubocop:disable-next Naming/PredicateMethod -- reports has-surface AND appends parts (a side-effecting builder)
|
|
155
155
|
def collect_producer_parts(plugin, id, parts, opaque)
|
|
156
156
|
producer_ids = plugin.class.producers.keys
|
|
157
157
|
return false if producer_ids.empty?
|
|
@@ -163,7 +163,6 @@ module Rigor
|
|
|
163
163
|
end
|
|
164
164
|
true
|
|
165
165
|
end
|
|
166
|
-
# rubocop:enable Naming/PredicateMethod
|
|
167
166
|
|
|
168
167
|
# Channel (c): the optional {Plugin::Base#incremental_state_fingerprint} hook. Returns true when the
|
|
169
168
|
# plugin defines it (it has a surface).
|
|
@@ -54,16 +54,25 @@ module Rigor
|
|
|
54
54
|
reachable = walk(edges, seeds: production_seeds | test_seeds, roles: %i[production task config test])
|
|
55
55
|
unreached = @owned - reachable
|
|
56
56
|
namespaces = namespace_only(unreached, reachable)
|
|
57
|
-
|
|
57
|
+
# The data-file demotion applies to BOTH buckets it can speak to, which is what the tier
|
|
58
|
+
# contract says and what the implementation had narrowed (#370). See {#tainted}.
|
|
59
|
+
test_only = reachable - production
|
|
60
|
+
undecidable = tainted(unreached - namespaces).merge(tainted(test_only))
|
|
61
|
+
build_report(edges: edges, reachable: reachable, unreached: unreached, namespaces: namespaces,
|
|
62
|
+
test_only: test_only, undecidable: undecidable)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def build_report(edges:, reachable:, unreached:, namespaces:, test_only:, undecidable:)
|
|
68
|
+
demoted = undecidable.keys.to_set
|
|
58
69
|
Report.new(declared: @owned.size, reachable: reachable.size,
|
|
59
|
-
candidates: rows(unreached - namespaces -
|
|
70
|
+
candidates: rows(unreached - namespaces - demoted),
|
|
60
71
|
undecidable: undecidable.map { |fqn, reason| undecidable_row(fqn, reason) }.freeze,
|
|
61
|
-
test_only: rows(
|
|
72
|
+
test_only: rows(test_only - demoted),
|
|
62
73
|
namespaces: namespaces.size, roots: production_seeds.size, edges: edges.size)
|
|
63
74
|
end
|
|
64
75
|
|
|
65
|
-
private
|
|
66
|
-
|
|
67
76
|
# A literal-argument `"Foo::Bar".constantize` names its constant exactly, so it is a REFERENCE, not an
|
|
68
77
|
# unknown. Keeping this distinct from the taint below is what stops the tier being a blanket namespace
|
|
69
78
|
# poison — Rigor knows the argument's shape, and a type-free indexer does not.
|
|
@@ -72,21 +81,32 @@ module Rigor
|
|
|
72
81
|
next if use.name.nil?
|
|
73
82
|
|
|
74
83
|
Scan::Reference.new(as_written: use.name.sub(/\A::/, ""), nesting: [].freeze, from: nil,
|
|
75
|
-
role: :production, path: use.path, line: use.line
|
|
84
|
+
role: :production, path: use.path, line: use.line,
|
|
85
|
+
rooted: use.name.start_with?("::"))
|
|
76
86
|
end
|
|
77
87
|
end
|
|
78
88
|
|
|
79
|
-
# `{fqn => reason}` for every
|
|
89
|
+
# `{fqn => reason}` for every declaration in `fqns` a dynamic site could still be naming. A site with a
|
|
80
90
|
# literal prefix taints that namespace and everything under it; a site with no prefix at all cannot be
|
|
81
91
|
# bounded, so it taints nothing rather than everything — poisoning the whole project would empty the
|
|
82
92
|
# report and teach the reader that the tier means nothing.
|
|
83
|
-
|
|
84
|
-
|
|
93
|
+
#
|
|
94
|
+
# Asked of the unreached AND of the test-only set (#370). The tier contract says a name appearing in a
|
|
95
|
+
# data file "MUST demote to this tier"; the implementation had asked only about the unreached, so a
|
|
96
|
+
# declaration a spec references AND `config/recurring.yml` names kept its data-file evidence discarded
|
|
97
|
+
# and landed under "live test, dead production path". That heading is an assertion about production,
|
|
98
|
+
# and a scheduler entry is evidence against it — the reported case runs every three minutes.
|
|
99
|
+
#
|
|
100
|
+
# Both buckets ask a different question of the same ambiguity ("is this dead?" against "is the
|
|
101
|
+
# production path dead?"), and the answer for both is that this reading cannot settle it. The reason
|
|
102
|
+
# string is what tells the two apart for a reader, so it names the evidence rather than the bucket.
|
|
103
|
+
def tainted(fqns)
|
|
104
|
+
sites = @dynamic_uses.select { |use| use.name.nil? && use.prefix }
|
|
85
105
|
|
|
86
|
-
return {} if
|
|
106
|
+
return {} if sites.empty?
|
|
87
107
|
|
|
88
|
-
|
|
89
|
-
|
|
108
|
+
fqns.each_with_object({}) do |fqn, out|
|
|
109
|
+
use = sites.find { |site| site.taints?(fqn) }
|
|
90
110
|
out[fqn] = use.site.nil? ? use.reason : "#{use.reason} (#{use.site})" if use
|
|
91
111
|
end
|
|
92
112
|
end
|
|
@@ -129,7 +149,7 @@ module Rigor
|
|
|
129
149
|
@references.each do |ref|
|
|
130
150
|
next unless yield(ref)
|
|
131
151
|
|
|
132
|
-
target = resolve(ref.as_written, ref.nesting)
|
|
152
|
+
target = resolve(ref.as_written, ref.nesting, rooted: ref.rooted)
|
|
133
153
|
set << target if target && @owned.include?(target)
|
|
134
154
|
end
|
|
135
155
|
set
|
|
@@ -138,7 +158,7 @@ module Rigor
|
|
|
138
158
|
# `[from_fqn_or_nil, to_fqn, role]` for every reference that resolves to an owned declaration.
|
|
139
159
|
def resolved_edges
|
|
140
160
|
@resolved_edges ||= @references.filter_map do |ref|
|
|
141
|
-
target = resolve(ref.as_written, ref.nesting)
|
|
161
|
+
target = resolve(ref.as_written, ref.nesting, rooted: ref.rooted)
|
|
142
162
|
next unless target && @owned.include?(target)
|
|
143
163
|
next if ref.from == target # a declaration referencing itself is not evidence of use
|
|
144
164
|
|
|
@@ -178,8 +198,16 @@ module Rigor
|
|
|
178
198
|
end
|
|
179
199
|
|
|
180
200
|
# Ruby's constant lookup at name granularity: `Module.nesting` innermost first, then the ancestors of
|
|
181
|
-
# the innermost cresting scope (#354), then the bare name.
|
|
182
|
-
|
|
201
|
+
# the innermost cresting scope (#354), then the bare name. When `rooted: true`, lexical nesting and
|
|
202
|
+
# ancestors are skipped, answering the top-level declaration directly (#625).
|
|
203
|
+
def resolve(as_written, nesting, rooted: false)
|
|
204
|
+
if rooted
|
|
205
|
+
return as_written if @by_fqn.key?(as_written)
|
|
206
|
+
|
|
207
|
+
idx = as_written.rindex("::")
|
|
208
|
+
return idx ? resolve(as_written[0, idx], nesting, rooted: true) : nil
|
|
209
|
+
end
|
|
210
|
+
|
|
183
211
|
walker = nesting.dup
|
|
184
212
|
until walker.empty?
|
|
185
213
|
candidate = (walker + [as_written]).join("::")
|
|
@@ -86,12 +86,16 @@ module Rigor
|
|
|
86
86
|
# @param configuration [Rigor::Configuration] the loaded project configuration.
|
|
87
87
|
# @param plugin_requirer [#call] how a plugin gem is brought into the process. The same seam
|
|
88
88
|
# `Analysis::Runner` exposes, so a spec can register a plugin class without publishing a gem.
|
|
89
|
+
# @param cache_store [Rigor::Cache::Store, nil] when given, each plugin's `#prepare` producers read
|
|
90
|
+
# and write the same ADR-60 record-and-validate slots they use under `rigor check`, instead of
|
|
91
|
+
# recomputing from scratch — a routes parse or a factory discovery is a validated cache read on
|
|
92
|
+
# every invocation after the first. Nil keeps the historical recompute-always behaviour.
|
|
89
93
|
# @return [Contribution] sorted and de-duplicated. Empty whenever the project declares no plugins, no
|
|
90
94
|
# plugin contributes anything, or anything at all goes wrong.
|
|
91
|
-
def collect(configuration:, plugin_requirer: ->(name) { require name })
|
|
95
|
+
def collect(configuration:, plugin_requirer: ->(name) { require name }, cache_store: nil)
|
|
92
96
|
return Contribution.empty if configuration.plugins.empty?
|
|
93
97
|
|
|
94
|
-
services = build_services(configuration)
|
|
98
|
+
services = build_services(configuration, cache_store)
|
|
95
99
|
registry = Plugin::Loader.load(configuration: configuration, services: services,
|
|
96
100
|
requirer: plugin_requirer)
|
|
97
101
|
return Contribution.empty if registry.nil? || registry.empty?
|
|
@@ -102,16 +106,15 @@ module Rigor
|
|
|
102
106
|
Contribution.empty
|
|
103
107
|
end
|
|
104
108
|
|
|
105
|
-
# Mirrors `
|
|
106
|
-
#
|
|
107
|
-
#
|
|
108
|
-
|
|
109
|
-
def build_services(configuration)
|
|
109
|
+
# Mirrors `Analysis::WorkerSession`'s services: the shared fact store plus whatever cache store the
|
|
110
|
+
# caller holds. Holding the `Services` is what gives access to the fact store the loaded plugins
|
|
111
|
+
# share — the loader hands the same instance to every plugin.
|
|
112
|
+
def build_services(configuration, cache_store)
|
|
110
113
|
Plugin::Services.new(
|
|
111
114
|
reflection: Reflection,
|
|
112
115
|
type: Type::Combinator,
|
|
113
116
|
configuration: configuration,
|
|
114
|
-
cache_store:
|
|
117
|
+
cache_store: cache_store
|
|
115
118
|
)
|
|
116
119
|
end
|
|
117
120
|
|